@ouro.bot/cli 0.1.0-alpha.816 → 0.1.0-alpha.818
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/assets/sanctuary-host-launcher.sh +16 -0
- package/changelog.json +16 -0
- package/deploy/unraid/Dockerfile +6 -0
- package/deploy/unraid/README.txt +296 -62
- package/deploy/unraid/docker-man-template-transaction.mjs +192 -7
- package/deploy/unraid/sanctuary-acceptance-adapter.sh +1 -1
- package/deploy/unraid/sanctuary-acceptance-contract.json +7 -7
- package/deploy/unraid/sanctuary-authority-installation.json +36 -0
- package/deploy/unraid/sanctuary-authority-service.sh +30 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +62 -9
- package/deploy/unraid/sanctuary-unit16-run.sh +15 -15
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
- package/deploy/unraid/sanctuary.xml +2 -1
- package/dist/heart/core.js +2 -1
- package/dist/heart/daemon/container-spec-auditor-main.js +8 -7
- package/dist/heart/daemon/container-spec-auditor.js +16 -9
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +95 -91
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +95 -169
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +4 -5
- package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
- package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
- package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
- package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +780 -0
- package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
- package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
- package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
- package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
- package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
- package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
- package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-service.js +615 -0
- package/dist/heart/daemon/sense-manager.js +20 -10
- package/dist/repertoire/tools-sanctuary-host.js +202 -0
- package/dist/repertoire/tools.js +15 -6
- package/dist/senses/private-runtime.js +2 -2
- package/dist/senses/root-host-approval-port.js +345 -0
- package/dist/senses/root-host-approval-runtime.js +393 -0
- package/dist/senses/sanctuary-authority-resident.js +102 -0
- package/dist/senses/telegram-admission.js +7 -0
- package/dist/senses/telegram-attachments.js +5 -1
- package/dist/senses/telegram-authority-transport.js +231 -0
- package/dist/senses/telegram-client.js +31 -8
- package/dist/senses/telegram-entry.js +1 -1
- package/dist/senses/telegram.js +168 -28
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FileSanctuaryTelegramAuthorityGateway = void 0;
|
|
37
|
+
exports.sanctuaryTelegramAuthorityStatePath = sanctuaryTelegramAuthorityStatePath;
|
|
38
|
+
exports.sanctuaryAuthorityPublicKeyDigest = sanctuaryAuthorityPublicKeyDigest;
|
|
39
|
+
const node_crypto_1 = require("node:crypto");
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
43
|
+
const session_transaction_1 = require("../../mind/session-transaction");
|
|
44
|
+
const sanctuary_authority_codec_1 = require("./sanctuary-authority-codec");
|
|
45
|
+
const SCHEMA_VERSION = 1;
|
|
46
|
+
const OBSERVATION_DOMAIN = "ouro.sanctuary.telegram-observation.v1";
|
|
47
|
+
const RAW_UPDATE_DIGEST = /^tgu_[A-Za-z0-9_-]{43}$/u;
|
|
48
|
+
const DECIMAL_ID = /^[1-9][0-9]*$/u;
|
|
49
|
+
const NONCE = /^[A-Za-z0-9_-]{43}$/u;
|
|
50
|
+
const ADMISSION_ID = /^[a-f0-9]{20}$/u;
|
|
51
|
+
const MAX_AUTHORIZED_CHATS = 64;
|
|
52
|
+
const AUTHORIZED_CHAT_IDLE_MS = 365 * 24 * 60 * 60 * 1_000;
|
|
53
|
+
function sanctuaryTelegramAuthorityStatePath(agentRoot) {
|
|
54
|
+
return path.join(agentRoot, "authority", "telegram-state.json");
|
|
55
|
+
}
|
|
56
|
+
function sanctuaryAuthorityPublicKeyDigest(key) {
|
|
57
|
+
const bytes = (0, node_crypto_1.createPublicKey)(key).export({ type: "spki", format: "der" });
|
|
58
|
+
return `sha256:${(0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex")}`;
|
|
59
|
+
}
|
|
60
|
+
function isObject(value) {
|
|
61
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
function exactKeys(value, keys) {
|
|
64
|
+
const actual = Object.keys(value).sort();
|
|
65
|
+
const expected = [...keys].sort();
|
|
66
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
67
|
+
}
|
|
68
|
+
function validTime(value) {
|
|
69
|
+
if (typeof value !== "string")
|
|
70
|
+
return false;
|
|
71
|
+
try {
|
|
72
|
+
return new Date(value).toISOString() === value;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function rawUpdateDigest(update) {
|
|
79
|
+
return `tgu_${(0, node_crypto_1.createHash)("sha256")
|
|
80
|
+
.update(`ouroboros.telegram.update.v1\0${JSON.stringify(update)}`, "utf8")
|
|
81
|
+
.digest("base64url")}`;
|
|
82
|
+
}
|
|
83
|
+
function residentUpdate(update) {
|
|
84
|
+
const callback = update.callback_query;
|
|
85
|
+
if (!callback?.data?.startsWith("ouh:") || !callback.message)
|
|
86
|
+
return structuredClone(update);
|
|
87
|
+
return {
|
|
88
|
+
update_id: update.update_id,
|
|
89
|
+
callback_query: {
|
|
90
|
+
id: callback.id,
|
|
91
|
+
from: { id: callback.from.id },
|
|
92
|
+
message: { message_id: callback.message.message_id, chat: { id: callback.message.chat.id } },
|
|
93
|
+
data: "root-host-callback",
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function validUpdate(value) {
|
|
98
|
+
return isObject(value) && Number.isSafeInteger(value.update_id) && value.update_id >= 0;
|
|
99
|
+
}
|
|
100
|
+
function updateCoordinates(update) {
|
|
101
|
+
const callback = update.callback_query;
|
|
102
|
+
if (callback
|
|
103
|
+
&& Number.isSafeInteger(callback.from?.id)
|
|
104
|
+
&& callback.message
|
|
105
|
+
&& Number.isSafeInteger(callback.message.message_id)
|
|
106
|
+
&& Number.isSafeInteger(callback.message.chat?.id)
|
|
107
|
+
&& typeof callback.id === "string"
|
|
108
|
+
&& callback.id.length > 0) {
|
|
109
|
+
return {
|
|
110
|
+
updateClass: "callback",
|
|
111
|
+
userId: String(callback.from.id),
|
|
112
|
+
chatId: String(callback.message.chat.id),
|
|
113
|
+
messageId: String(callback.message.message_id),
|
|
114
|
+
callbackQueryId: callback.id,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const message = update.message;
|
|
118
|
+
if (message
|
|
119
|
+
&& message.chat?.type === "private"
|
|
120
|
+
&& message.from
|
|
121
|
+
&& Number.isSafeInteger(message.from.id)
|
|
122
|
+
&& Number.isSafeInteger(message.chat.id)
|
|
123
|
+
&& Number.isSafeInteger(message.message_id)) {
|
|
124
|
+
return {
|
|
125
|
+
updateClass: "message",
|
|
126
|
+
userId: String(message.from.id),
|
|
127
|
+
chatId: String(message.chat.id),
|
|
128
|
+
messageId: String(message.message_id),
|
|
129
|
+
callbackQueryId: null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
function initialState() {
|
|
135
|
+
return { schemaVersion: SCHEMA_VERSION, cursor: 0, records: {}, authorizedChats: {} };
|
|
136
|
+
}
|
|
137
|
+
function validateObservation(value) {
|
|
138
|
+
if (!isObject(value)
|
|
139
|
+
|| !exactKeys(value, ["schemaVersion", "domain", "keyId", "payload", "signature"])
|
|
140
|
+
|| value.schemaVersion !== 1
|
|
141
|
+
|| value.domain !== OBSERVATION_DOMAIN
|
|
142
|
+
|| typeof value.keyId !== "string"
|
|
143
|
+
|| value.keyId.length === 0
|
|
144
|
+
|| typeof value.signature !== "string"
|
|
145
|
+
|| !isObject(value.payload)) {
|
|
146
|
+
throw new Error("Sanctuary Telegram authority observation is malformed");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function validateRecord(value, updateId) {
|
|
150
|
+
if (!isObject(value)
|
|
151
|
+
|| !exactKeys(value, ["updateId", "rawUpdateDigest", "rawUpdate", "disposition", "settlement", "observation", ...(Object.hasOwn(value, "deliveryUpdate") ? ["deliveryUpdate"] : [])])
|
|
152
|
+
|| value.updateId !== updateId
|
|
153
|
+
|| typeof value.rawUpdateDigest !== "string"
|
|
154
|
+
|| !RAW_UPDATE_DIGEST.test(value.rawUpdateDigest)
|
|
155
|
+
|| !isObject(value.rawUpdate)
|
|
156
|
+
|| value.rawUpdate.update_id !== updateId) {
|
|
157
|
+
throw new Error(`Sanctuary Telegram authority record ${updateId} is malformed`);
|
|
158
|
+
}
|
|
159
|
+
if (value.disposition === "ignored") {
|
|
160
|
+
if (value.settlement !== "ignored" || value.observation !== null) {
|
|
161
|
+
throw new Error(`Sanctuary Telegram authority ignored record ${updateId} is ambiguous`);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (value.disposition !== "dispatch"
|
|
166
|
+
|| !["pending", "completed", "indeterminate"].includes(String(value.settlement))) {
|
|
167
|
+
throw new Error(`Sanctuary Telegram authority record ${updateId} has invalid settlement`);
|
|
168
|
+
}
|
|
169
|
+
validateObservation(value.observation);
|
|
170
|
+
if (Object.hasOwn(value, "deliveryUpdate") && JSON.stringify(value.deliveryUpdate) !== JSON.stringify(residentUpdate(value.rawUpdate))) {
|
|
171
|
+
throw new Error("Sanctuary Telegram authority delivery projection changed");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function validateState(value) {
|
|
175
|
+
if (!isObject(value)
|
|
176
|
+
|| !exactKeys(value, ["schemaVersion", "cursor", "records", "authorizedChats"])
|
|
177
|
+
|| value.schemaVersion !== SCHEMA_VERSION
|
|
178
|
+
|| !Number.isSafeInteger(value.cursor)
|
|
179
|
+
|| value.cursor < 0
|
|
180
|
+
|| !isObject(value.records)
|
|
181
|
+
|| !isObject(value.authorizedChats)) {
|
|
182
|
+
throw new Error("Sanctuary Telegram authority state is malformed");
|
|
183
|
+
}
|
|
184
|
+
for (const [key, record] of Object.entries(value.records)) {
|
|
185
|
+
if (!/^(?:0|[1-9][0-9]*)$/u.test(key)) {
|
|
186
|
+
throw new Error("Sanctuary Telegram authority state has an invalid update key");
|
|
187
|
+
}
|
|
188
|
+
validateRecord(record, Number(key));
|
|
189
|
+
}
|
|
190
|
+
if (Object.keys(value.authorizedChats).length > MAX_AUTHORIZED_CHATS) {
|
|
191
|
+
throw new Error("Sanctuary Telegram authority chat registry exceeds its limit");
|
|
192
|
+
}
|
|
193
|
+
for (const [key, entry] of Object.entries(value.authorizedChats)) {
|
|
194
|
+
if (!isObject(entry)
|
|
195
|
+
|| !exactKeys(entry, ["admissionId", "updateId", "userId", "chatId", "admittedAt", "lastObservedAt"])
|
|
196
|
+
|| key !== `${entry.userId}:${entry.chatId}`
|
|
197
|
+
|| !ADMISSION_ID.test(String(entry.admissionId))
|
|
198
|
+
|| !Number.isSafeInteger(entry.updateId)
|
|
199
|
+
|| entry.updateId < 0
|
|
200
|
+
|| !DECIMAL_ID.test(String(entry.userId))
|
|
201
|
+
|| !DECIMAL_ID.test(String(entry.chatId))
|
|
202
|
+
|| !validTime(entry.admittedAt)
|
|
203
|
+
|| !validTime(entry.lastObservedAt)) {
|
|
204
|
+
throw new Error("Sanctuary Telegram authority chat registry is malformed");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function transactionState(transaction) {
|
|
209
|
+
if (transaction.value === null) {
|
|
210
|
+
if (transaction.bytes)
|
|
211
|
+
throw new Error("Sanctuary Telegram authority state is malformed");
|
|
212
|
+
return initialState();
|
|
213
|
+
}
|
|
214
|
+
validateState(transaction.value);
|
|
215
|
+
return transaction.value;
|
|
216
|
+
}
|
|
217
|
+
class FileSanctuaryTelegramAuthorityGateway {
|
|
218
|
+
#statePath;
|
|
219
|
+
#options;
|
|
220
|
+
constructor(agentRoot, options) {
|
|
221
|
+
if (typeof agentRoot !== "string" || agentRoot.length === 0) {
|
|
222
|
+
throw new Error("Sanctuary Telegram authority root is invalid");
|
|
223
|
+
}
|
|
224
|
+
for (const [label, value] of [
|
|
225
|
+
["bot id", options.botId],
|
|
226
|
+
["owner user id", options.ownerUserId],
|
|
227
|
+
["owner chat id", options.ownerChatId],
|
|
228
|
+
]) {
|
|
229
|
+
if (!DECIMAL_ID.test(value))
|
|
230
|
+
throw new Error(`Sanctuary Telegram authority ${label} is invalid`);
|
|
231
|
+
}
|
|
232
|
+
if (!options.targetHost
|
|
233
|
+
|| !options.keyId
|
|
234
|
+
|| options.publicKeyDigest !== sanctuaryAuthorityPublicKeyDigest(options.privateKey)) {
|
|
235
|
+
throw new Error("Sanctuary Telegram authority issuer configuration is invalid");
|
|
236
|
+
}
|
|
237
|
+
this.#statePath = sanctuaryTelegramAuthorityStatePath(agentRoot);
|
|
238
|
+
this.#options = {
|
|
239
|
+
...options,
|
|
240
|
+
now: options.now ?? (() => new Date().toISOString()),
|
|
241
|
+
nonce: options.nonce ?? (() => (0, node_crypto_1.randomBytes)(32).toString("base64url")),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
initializeCursor(cursor) {
|
|
245
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0)
|
|
246
|
+
throw new Error("Sanctuary Telegram migration cursor is invalid");
|
|
247
|
+
(0, session_transaction_1.withImmediateSessionTurnLease)(this.#statePath, (lease) => {
|
|
248
|
+
const transaction = (0, session_transaction_1.readSessionTransaction)(this.#statePath, lease);
|
|
249
|
+
const state = transactionState(transaction);
|
|
250
|
+
if (Object.keys(state.records).length !== 0 || (transaction.value !== null && state.cursor !== cursor))
|
|
251
|
+
throw new Error("Sanctuary Telegram migration cursor is already owned");
|
|
252
|
+
if (transaction.value !== null)
|
|
253
|
+
return;
|
|
254
|
+
state.cursor = cursor;
|
|
255
|
+
this.#write(state, transaction.revision, lease);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
cursorSnapshot() {
|
|
259
|
+
return this.#read((state) => {
|
|
260
|
+
const records = Object.values(state.records).sort((a, b) => a.updateId - b.updateId);
|
|
261
|
+
const progress = { cursor: state.cursor, records: records.map(({ updateId, rawUpdateDigest, settlement }) => ({ updateId, rawUpdateDigest, settlement })) };
|
|
262
|
+
return (0, sanctuary_authority_codec_1.signAuthorityPayload)({
|
|
263
|
+
domain: "ouro.sanctuary.telegram-cursor.v1",
|
|
264
|
+
keyId: this.#options.keyId,
|
|
265
|
+
privateKey: this.#options.privateKey,
|
|
266
|
+
payload: {
|
|
267
|
+
...this.identity(),
|
|
268
|
+
cursor: state.cursor,
|
|
269
|
+
pendingUpdateIds: records.filter((record) => record.settlement === "pending").map((record) => record.updateId),
|
|
270
|
+
progressDigest: (0, sanctuary_authority_codec_1.authorityArtifactDigest)("ouro.sanctuary.telegram-progress.v1", progress),
|
|
271
|
+
observedAt: this.#now(),
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}, true);
|
|
275
|
+
}
|
|
276
|
+
capture(updates) {
|
|
277
|
+
if (!Array.isArray(updates))
|
|
278
|
+
throw new Error("Sanctuary Telegram authority updates must be an array");
|
|
279
|
+
(0, session_transaction_1.withImmediateSessionTurnLease)(this.#statePath, (lease) => {
|
|
280
|
+
const transaction = (0, session_transaction_1.readSessionTransaction)(this.#statePath, lease);
|
|
281
|
+
const state = transactionState(transaction);
|
|
282
|
+
let changed = false;
|
|
283
|
+
for (const update of updates) {
|
|
284
|
+
if (!validUpdate(update)) {
|
|
285
|
+
throw new Error("Sanctuary Telegram authority update is invalid");
|
|
286
|
+
}
|
|
287
|
+
const updateId = update.update_id;
|
|
288
|
+
const digest = rawUpdateDigest(update);
|
|
289
|
+
const existing = state.records[String(updateId)];
|
|
290
|
+
if (existing) {
|
|
291
|
+
if (existing.rawUpdateDigest !== digest) {
|
|
292
|
+
throw new Error(`Sanctuary Telegram authority duplicate update ${updateId} changed`);
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (updateId < state.cursor) {
|
|
297
|
+
throw new Error(`Sanctuary Telegram authority update ${updateId} is stale`);
|
|
298
|
+
}
|
|
299
|
+
const coordinates = updateCoordinates(update);
|
|
300
|
+
if (!coordinates) {
|
|
301
|
+
state.records[String(updateId)] = {
|
|
302
|
+
updateId,
|
|
303
|
+
rawUpdateDigest: digest,
|
|
304
|
+
rawUpdate: structuredClone(update),
|
|
305
|
+
disposition: "ignored",
|
|
306
|
+
settlement: "ignored",
|
|
307
|
+
observation: null,
|
|
308
|
+
};
|
|
309
|
+
changed = true;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const observedAt = this.#options.now();
|
|
313
|
+
if (!validTime(observedAt))
|
|
314
|
+
throw new Error("Sanctuary Telegram authority clock is invalid");
|
|
315
|
+
this.#pruneAuthorizedChats(state, observedAt);
|
|
316
|
+
const admitted = state.authorizedChats[`${coordinates.userId}:${coordinates.chatId}`];
|
|
317
|
+
if (admitted)
|
|
318
|
+
admitted.lastObservedAt = observedAt;
|
|
319
|
+
const nonce = this.#options.nonce();
|
|
320
|
+
if (!NONCE.test(nonce))
|
|
321
|
+
throw new Error("Sanctuary Telegram authority nonce is invalid");
|
|
322
|
+
if (Object.values(state.records).some((record) => record.observation?.payload.nonce === nonce)) {
|
|
323
|
+
throw new Error("Sanctuary Telegram authority nonce is already used");
|
|
324
|
+
}
|
|
325
|
+
const payload = {
|
|
326
|
+
targetHost: this.#options.targetHost,
|
|
327
|
+
botId: this.#options.botId,
|
|
328
|
+
updateId,
|
|
329
|
+
...coordinates,
|
|
330
|
+
ownerEligible: coordinates.userId === this.#options.ownerUserId
|
|
331
|
+
&& coordinates.chatId === this.#options.ownerChatId,
|
|
332
|
+
rawUpdateDigest: digest,
|
|
333
|
+
...(rawUpdateDigest(residentUpdate(update)) !== digest ? { deliveryUpdateDigest: rawUpdateDigest(residentUpdate(update)) } : {}),
|
|
334
|
+
observedAt,
|
|
335
|
+
settlement: "pending",
|
|
336
|
+
nonce,
|
|
337
|
+
publicKeyDigest: this.#options.publicKeyDigest,
|
|
338
|
+
};
|
|
339
|
+
state.records[String(updateId)] = {
|
|
340
|
+
updateId,
|
|
341
|
+
rawUpdateDigest: digest,
|
|
342
|
+
rawUpdate: structuredClone(update),
|
|
343
|
+
disposition: "dispatch",
|
|
344
|
+
settlement: "pending",
|
|
345
|
+
observation: (0, sanctuary_authority_codec_1.signAuthorityPayload)({
|
|
346
|
+
domain: OBSERVATION_DOMAIN,
|
|
347
|
+
keyId: this.#options.keyId,
|
|
348
|
+
payload,
|
|
349
|
+
privateKey: this.#options.privateKey,
|
|
350
|
+
}),
|
|
351
|
+
...(rawUpdateDigest(residentUpdate(update)) !== digest ? { deliveryUpdate: residentUpdate(update) } : {}),
|
|
352
|
+
};
|
|
353
|
+
changed = true;
|
|
354
|
+
}
|
|
355
|
+
this.#advanceCursor(state);
|
|
356
|
+
if (changed)
|
|
357
|
+
this.#write(state, transaction.revision, lease);
|
|
358
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_telegram_capture_settled", message: "Sanctuary Telegram ingress capture settled", meta: { changed } });
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
poll() {
|
|
362
|
+
return this.#read((state) => {
|
|
363
|
+
const record = Object.values(state.records)
|
|
364
|
+
.filter((candidate) => candidate.disposition === "dispatch"
|
|
365
|
+
&& candidate.settlement === "pending"
|
|
366
|
+
&& candidate.updateId >= state.cursor)
|
|
367
|
+
.sort((left, right) => left.updateId - right.updateId)[0];
|
|
368
|
+
return record?.observation ?? null;
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
settle(settlement) {
|
|
372
|
+
if (!isObject(settlement)
|
|
373
|
+
|| !exactKeys(settlement, ["updateId", "observationDigest", "outcome"])
|
|
374
|
+
|| !Number.isSafeInteger(settlement.updateId)
|
|
375
|
+
|| settlement.updateId < 0
|
|
376
|
+
|| typeof settlement.observationDigest !== "string"
|
|
377
|
+
|| !/^sha256:[a-f0-9]{64}$/u.test(settlement.observationDigest)
|
|
378
|
+
|| !["completed", "indeterminate"].includes(String(settlement.outcome))) {
|
|
379
|
+
throw new Error("Sanctuary Telegram authority settlement is malformed");
|
|
380
|
+
}
|
|
381
|
+
(0, session_transaction_1.withImmediateSessionTurnLease)(this.#statePath, (lease) => {
|
|
382
|
+
const transaction = (0, session_transaction_1.readSessionTransaction)(this.#statePath, lease);
|
|
383
|
+
const state = transactionState(transaction);
|
|
384
|
+
const record = state.records[String(settlement.updateId)];
|
|
385
|
+
if (!record || record.disposition !== "dispatch") {
|
|
386
|
+
throw new Error(`Sanctuary Telegram authority update ${settlement.updateId} is missing`);
|
|
387
|
+
}
|
|
388
|
+
if (record.settlement !== "pending") {
|
|
389
|
+
throw new Error(`Sanctuary Telegram authority update ${settlement.updateId} is already settled`);
|
|
390
|
+
}
|
|
391
|
+
const digest = (0, sanctuary_authority_codec_1.authorityArtifactDigest)(record.observation.domain, record.observation.payload);
|
|
392
|
+
if (digest !== settlement.observationDigest) {
|
|
393
|
+
throw new Error(`Sanctuary Telegram authority observation digest changed for ${settlement.updateId}`);
|
|
394
|
+
}
|
|
395
|
+
record.settlement = settlement.outcome;
|
|
396
|
+
this.#advanceCursor(state);
|
|
397
|
+
this.#write(state, transaction.revision, lease);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
cursor() {
|
|
401
|
+
return this.#read((state) => state.cursor);
|
|
402
|
+
}
|
|
403
|
+
identity() {
|
|
404
|
+
return {
|
|
405
|
+
targetHost: this.#options.targetHost,
|
|
406
|
+
botId: this.#options.botId,
|
|
407
|
+
ownerUserId: this.#options.ownerUserId,
|
|
408
|
+
ownerChatId: this.#options.ownerChatId,
|
|
409
|
+
keyId: this.#options.keyId,
|
|
410
|
+
publicKeyDigest: this.#options.publicKeyDigest,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
ownsCallbackQuery(callbackQueryId) {
|
|
414
|
+
if (typeof callbackQueryId !== "string" || callbackQueryId.length === 0)
|
|
415
|
+
return false;
|
|
416
|
+
return this.#read((state) => Object.values(state.records).some((record) => record.disposition === "dispatch"
|
|
417
|
+
&& record.rawUpdate.callback_query?.id === callbackQueryId));
|
|
418
|
+
}
|
|
419
|
+
ownsFileId(fileId) {
|
|
420
|
+
if (typeof fileId !== "string" || fileId.length === 0)
|
|
421
|
+
return false;
|
|
422
|
+
return this.#read((state) => Object.values(state.records).some((record) => {
|
|
423
|
+
if (record.disposition !== "dispatch")
|
|
424
|
+
return false;
|
|
425
|
+
const message = record.rawUpdate.message;
|
|
426
|
+
return message?.document?.file_id === fileId
|
|
427
|
+
|| message?.audio?.file_id === fileId
|
|
428
|
+
|| message?.video?.file_id === fileId
|
|
429
|
+
|| message?.voice?.file_id === fileId
|
|
430
|
+
|| message?.animation?.file_id === fileId
|
|
431
|
+
|| message?.sticker?.file_id === fileId
|
|
432
|
+
|| message?.photo?.some((photo) => photo.file_id === fileId) === true;
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
ownsCurrentObservation(input) {
|
|
436
|
+
if (!Number.isSafeInteger(input.updateId)
|
|
437
|
+
|| input.updateId < 0
|
|
438
|
+
|| !/^sha256:[a-f0-9]{64}$/u.test(input.observationDigest)
|
|
439
|
+
|| !DECIMAL_ID.test(input.chatId))
|
|
440
|
+
return false;
|
|
441
|
+
return this.#read((state) => {
|
|
442
|
+
const record = state.records[String(input.updateId)];
|
|
443
|
+
return Boolean(record
|
|
444
|
+
&& record.disposition === "dispatch"
|
|
445
|
+
&& record.settlement === "pending"
|
|
446
|
+
&& record.observation.payload.chatId === input.chatId
|
|
447
|
+
&& (0, sanctuary_authority_codec_1.authorityArtifactDigest)(record.observation.domain, record.observation.payload) === input.observationDigest);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
ownerObservation(input) {
|
|
451
|
+
if (!Number.isSafeInteger(input.updateId)
|
|
452
|
+
|| input.updateId < 0
|
|
453
|
+
|| !/^sha256:[a-f0-9]{64}$/u.test(input.observationDigest))
|
|
454
|
+
return null;
|
|
455
|
+
return this.#read((state) => {
|
|
456
|
+
const record = state.records[String(input.updateId)];
|
|
457
|
+
if (!record
|
|
458
|
+
|| record.disposition !== "dispatch"
|
|
459
|
+
|| record.settlement !== "pending"
|
|
460
|
+
|| !record.observation.payload.ownerEligible
|
|
461
|
+
|| (0, sanctuary_authority_codec_1.authorityArtifactDigest)(record.observation.domain, record.observation.payload) !== input.observationDigest)
|
|
462
|
+
return null;
|
|
463
|
+
return record.observation;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
admitChat(input) {
|
|
467
|
+
if (!ADMISSION_ID.test(input.admissionId)
|
|
468
|
+
|| !Number.isSafeInteger(input.updateId)
|
|
469
|
+
|| input.updateId < 0
|
|
470
|
+
|| !DECIMAL_ID.test(input.userId)
|
|
471
|
+
|| !DECIMAL_ID.test(input.chatId)) {
|
|
472
|
+
throw new Error("Sanctuary Telegram authority chat admission is malformed");
|
|
473
|
+
}
|
|
474
|
+
(0, session_transaction_1.withImmediateSessionTurnLease)(this.#statePath, (lease) => {
|
|
475
|
+
const transaction = (0, session_transaction_1.readSessionTransaction)(this.#statePath, lease);
|
|
476
|
+
const state = transactionState(transaction);
|
|
477
|
+
const record = state.records[String(input.updateId)];
|
|
478
|
+
if (!record
|
|
479
|
+
|| record.disposition !== "dispatch"
|
|
480
|
+
|| record.observation.payload.userId !== input.userId
|
|
481
|
+
|| record.observation.payload.chatId !== input.chatId
|
|
482
|
+
|| record.observation.payload.ownerEligible) {
|
|
483
|
+
throw new Error("Sanctuary Telegram authority chat admission is not root-observed");
|
|
484
|
+
}
|
|
485
|
+
const now = this.#now();
|
|
486
|
+
this.#pruneAuthorizedChats(state, now);
|
|
487
|
+
if (Date.parse(record.observation.payload.observedAt) <= Date.parse(now) - AUTHORIZED_CHAT_IDLE_MS) {
|
|
488
|
+
throw new Error("Sanctuary Telegram authority chat observation expired");
|
|
489
|
+
}
|
|
490
|
+
const key = `${input.userId}:${input.chatId}`;
|
|
491
|
+
const existing = state.authorizedChats[key];
|
|
492
|
+
if (existing) {
|
|
493
|
+
if (input.updateId <= existing.updateId && (existing.admissionId !== input.admissionId || existing.updateId !== input.updateId)) {
|
|
494
|
+
throw new Error("Sanctuary Telegram authority chat admission changed");
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (Object.keys(state.authorizedChats).length >= MAX_AUTHORIZED_CHATS) {
|
|
499
|
+
throw new Error("Sanctuary Telegram authority chat registry limit reached");
|
|
500
|
+
}
|
|
501
|
+
state.authorizedChats[key] = {
|
|
502
|
+
...input,
|
|
503
|
+
admittedAt: now,
|
|
504
|
+
lastObservedAt: record.observation.payload.observedAt,
|
|
505
|
+
};
|
|
506
|
+
this.#write(state, transaction.revision, lease);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
revokeChat(input) {
|
|
510
|
+
if (!DECIMAL_ID.test(input.userId) || !DECIMAL_ID.test(input.chatId)) {
|
|
511
|
+
throw new Error("Sanctuary Telegram authority chat revocation is malformed");
|
|
512
|
+
}
|
|
513
|
+
(0, session_transaction_1.withImmediateSessionTurnLease)(this.#statePath, (lease) => {
|
|
514
|
+
const transaction = (0, session_transaction_1.readSessionTransaction)(this.#statePath, lease);
|
|
515
|
+
const state = transactionState(transaction);
|
|
516
|
+
const key = `${input.userId}:${input.chatId}`;
|
|
517
|
+
if (!state.authorizedChats[key])
|
|
518
|
+
return;
|
|
519
|
+
delete state.authorizedChats[key];
|
|
520
|
+
this.#write(state, transaction.revision, lease);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
isAuthorizedChat(chatId) {
|
|
524
|
+
if (!DECIMAL_ID.test(chatId))
|
|
525
|
+
return false;
|
|
526
|
+
if (chatId === this.#options.ownerChatId)
|
|
527
|
+
return true;
|
|
528
|
+
return (0, session_transaction_1.withImmediateSessionTurnLease)(this.#statePath, (lease) => {
|
|
529
|
+
const transaction = (0, session_transaction_1.readSessionTransaction)(this.#statePath, lease);
|
|
530
|
+
const state = transactionState(transaction);
|
|
531
|
+
const now = this.#now();
|
|
532
|
+
const changed = this.#pruneAuthorizedChats(state, now);
|
|
533
|
+
const entry = Object.values(state.authorizedChats).find((candidate) => candidate.chatId === chatId);
|
|
534
|
+
if (changed)
|
|
535
|
+
this.#write(state, transaction.revision, lease);
|
|
536
|
+
return Boolean(entry);
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
record(updateId) {
|
|
540
|
+
if (!Number.isSafeInteger(updateId) || updateId < 0) {
|
|
541
|
+
throw new Error("Sanctuary Telegram authority update id is invalid");
|
|
542
|
+
}
|
|
543
|
+
return this.#read((state) => state.records[String(updateId)] ?? null);
|
|
544
|
+
}
|
|
545
|
+
#advanceCursor(state) {
|
|
546
|
+
const records = Object.values(state.records)
|
|
547
|
+
.filter((record) => record.updateId >= state.cursor)
|
|
548
|
+
.sort((left, right) => left.updateId - right.updateId);
|
|
549
|
+
for (const record of records) {
|
|
550
|
+
if (record.disposition === "dispatch" && record.settlement === "pending")
|
|
551
|
+
break;
|
|
552
|
+
state.cursor = record.updateId + 1;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
#now() {
|
|
556
|
+
const now = this.#options.now();
|
|
557
|
+
if (!validTime(now))
|
|
558
|
+
throw new Error("Sanctuary Telegram authority clock is invalid");
|
|
559
|
+
return now;
|
|
560
|
+
}
|
|
561
|
+
#pruneAuthorizedChats(state, now) {
|
|
562
|
+
let changed = false;
|
|
563
|
+
const cutoff = Date.parse(now) - AUTHORIZED_CHAT_IDLE_MS;
|
|
564
|
+
for (const [key, entry] of Object.entries(state.authorizedChats)) {
|
|
565
|
+
if (Date.parse(entry.lastObservedAt) <= cutoff) {
|
|
566
|
+
delete state.authorizedChats[key];
|
|
567
|
+
changed = true;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return changed;
|
|
571
|
+
}
|
|
572
|
+
#read(read, requirePersisted = false) {
|
|
573
|
+
return (0, session_transaction_1.withImmediateSessionTurnLease)(this.#statePath, (lease) => {
|
|
574
|
+
const transaction = (0, session_transaction_1.readSessionTransaction)(this.#statePath, lease);
|
|
575
|
+
if (requirePersisted && transaction.value === null)
|
|
576
|
+
throw new Error("Sanctuary Telegram authority cursor state is absent");
|
|
577
|
+
return read(transactionState(transaction));
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
#write(state, expectedRevision, lease) {
|
|
581
|
+
fs.chmodSync(path.dirname(this.#statePath), 0o700);
|
|
582
|
+
(0, session_transaction_1.writeSessionTransaction)(this.#statePath, state, { lease, expectedRevision });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
exports.FileSanctuaryTelegramAuthorityGateway = FileSanctuaryTelegramAuthorityGateway;
|