@ouro.bot/cli 0.1.0-alpha.741 → 0.1.0-alpha.743
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.json +42 -0
- package/deploy/unraid/Dockerfile +7 -2
- package/deploy/unraid/README.txt +717 -106
- package/deploy/unraid/migrate-sanctuary-bundle.mjs +21 -0
- package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
- package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
- package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
- package/deploy/unraid/ouro-events/install-usenet-guard.sh +434 -0
- package/deploy/unraid/ouro-events/usenet-health.sh +178 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +4 -3
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +5 -1
- package/deploy/unraid/sanctuary.ouro/agent.json +2 -2
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +2 -2
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +1 -1
- package/deploy/unraid/sanctuary.ouro/provider-readiness.json +3 -10
- package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -1
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -1
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +4 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +6 -2
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +4 -2
- package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +19 -17
- package/deploy/unraid/sanctuary.xml +12 -9
- package/dist/arc/cares.js +171 -37
- package/dist/arc/obligations.js +38 -0
- package/dist/heart/awaiting/await-alert.js +4 -2
- package/dist/heart/awaiting/await-expiry.js +39 -4
- package/dist/heart/awaiting/await-parser.js +8 -0
- package/dist/heart/context-loss-gauntlet.js +31 -2
- package/dist/heart/context-loss-sentinel.js +2 -0
- package/dist/heart/core.js +37 -7
- package/dist/heart/cross-chat-delivery.js +1 -0
- package/dist/heart/daemon/container-healthcheck.js +3 -1
- package/dist/heart/daemon/container-runtime.js +13 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +11 -7
- package/dist/heart/daemon/container-spec-auditor.js +38 -11
- package/dist/heart/daemon/daemon-entry.js +19 -5
- package/dist/heart/daemon/daemon.js +154 -22
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +151 -87
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +26 -29
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +42 -51
- package/dist/heart/daemon/sanctuary-bundle-migration.js +398 -0
- package/dist/heart/daemon/sanctuary-scheduler-origin.js +11 -5
- package/dist/heart/daemon/supercronic-supervisor.js +16 -1
- package/dist/heart/external-events/router.js +1025 -43
- package/dist/heart/provider-models.js +10 -3
- package/dist/heart/session-events.js +23 -5
- package/dist/heart/steward-policy.js +302 -0
- package/dist/heart/tool-approval.js +3 -1
- package/dist/heart/turn-context.js +1 -0
- package/dist/mind/context.js +8 -3
- package/dist/mind/pending.js +25 -0
- package/dist/mind/prompt.js +49 -1
- package/dist/repertoire/guardrails.js +2 -1
- package/dist/repertoire/relationship-authorization.js +194 -0
- package/dist/repertoire/tools-awaiting.js +229 -26
- package/dist/repertoire/tools-base.js +15 -0
- package/dist/repertoire/tools-continuity.js +259 -4
- package/dist/repertoire/tools-notes.js +29 -3
- package/dist/repertoire/tools-session.js +219 -4
- package/dist/repertoire/tools-steward-policy.js +88 -0
- package/dist/repertoire/tools-telegram-contacts.js +37 -0
- package/dist/repertoire/tools-unraid.js +113 -6
- package/dist/repertoire/tools.js +104 -9
- package/dist/repertoire/unraid-restart.js +86 -4
- package/dist/senses/private-runtime-worker.js +36 -3
- package/dist/senses/private-runtime.js +310 -63
- package/dist/senses/sanctuary-health-acceptance-probe.js +8 -6
- package/dist/senses/sanctuary-health-runner.js +51 -96
- package/dist/senses/sanctuary-health.js +54 -93
- package/dist/senses/sanctuary-runtime.js +36 -2
- package/dist/senses/sanctuary-sab.js +80 -0
- package/dist/senses/shared-turn.js +64 -9
- package/dist/senses/telegram-admission.js +684 -0
- package/dist/senses/telegram-approval-runtime.js +46 -6
- package/dist/senses/telegram-client.js +236 -72
- package/dist/senses/telegram-effect-adapter.js +787 -0
- package/dist/senses/telegram-entry.js +4 -1
- package/dist/senses/telegram.js +728 -48
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
|
@@ -0,0 +1,684 @@
|
|
|
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.FileTelegramAdmissionStore = exports.NEWLY_ADMITTED_ORIENTATION = exports.FIXED_ADMISSION_ACKNOWLEDGEMENT = void 0;
|
|
37
|
+
exports.createTelegramAdmissionController = createTelegramAdmissionController;
|
|
38
|
+
const crypto = __importStar(require("node:crypto"));
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const runtime_1 = require("../nerves/runtime");
|
|
42
|
+
const session_transaction_1 = require("../mind/session-transaction");
|
|
43
|
+
const telegram_effect_adapter_1 = require("./telegram-effect-adapter");
|
|
44
|
+
Object.defineProperty(exports, "FIXED_ADMISSION_ACKNOWLEDGEMENT", { enumerable: true, get: function () { return telegram_effect_adapter_1.FIXED_ADMISSION_ACKNOWLEDGEMENT; } });
|
|
45
|
+
const telegram_client_1 = require("./telegram-client");
|
|
46
|
+
exports.NEWLY_ADMITTED_ORIENTATION = {
|
|
47
|
+
kind: "newly_admitted",
|
|
48
|
+
instruction: "welcome_and_explain_capabilities",
|
|
49
|
+
};
|
|
50
|
+
const ACTIVE_STATUSES = new Set([
|
|
51
|
+
"pending", "approved", "friend_bound", "ingress_committed", "turn_queued",
|
|
52
|
+
]);
|
|
53
|
+
const TERMINAL_STATUSES = new Set([
|
|
54
|
+
"handled", "denied", "blocked", "expired", "collision", "indeterminate",
|
|
55
|
+
]);
|
|
56
|
+
const ADMISSION_ID = /^[a-f0-9]{20}$/u;
|
|
57
|
+
const CANONICAL_ID = /^[1-9][0-9]*$/u;
|
|
58
|
+
const DEFAULT_LIMITS = {
|
|
59
|
+
maxPendingContacts: 32,
|
|
60
|
+
maxTextBytes: 16 * 1024,
|
|
61
|
+
maxMessagesPerIdentity: 8,
|
|
62
|
+
maxMessagesPerWindow: 128,
|
|
63
|
+
rateWindowMs: 60_000,
|
|
64
|
+
maxTotalBytes: 256 * 1024,
|
|
65
|
+
retentionMs: 24 * 60 * 60 * 1_000,
|
|
66
|
+
retryCooldownMs: 5 * 60 * 1_000,
|
|
67
|
+
terminalRetentionMs: 7 * 24 * 60 * 60 * 1_000,
|
|
68
|
+
maxTerminalRecords: 256,
|
|
69
|
+
};
|
|
70
|
+
function admissionId(input) {
|
|
71
|
+
return crypto.createHash("sha256")
|
|
72
|
+
.update(`telegram-admission-v1\0${input.botId}\0${input.userId}\0${input.chatId}\0${input.updateId}`)
|
|
73
|
+
.digest("hex")
|
|
74
|
+
.slice(0, 20);
|
|
75
|
+
}
|
|
76
|
+
function digest(value) {
|
|
77
|
+
return crypto.createHash("sha256").update(value, "utf8").digest("hex");
|
|
78
|
+
}
|
|
79
|
+
function identity(record) {
|
|
80
|
+
return `${record.botId}:${record.userId}:${record.chatId}`;
|
|
81
|
+
}
|
|
82
|
+
function boundedLabel(value) {
|
|
83
|
+
return [...value.normalize("NFKC")].slice(0, 120).join("") || "(no Telegram label)";
|
|
84
|
+
}
|
|
85
|
+
function assertSafeInteger(value, label) {
|
|
86
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
87
|
+
throw new Error(`Telegram admission ${label} is invalid`);
|
|
88
|
+
}
|
|
89
|
+
function assertCanonicalId(value, label) {
|
|
90
|
+
if (!CANONICAL_ID.test(value))
|
|
91
|
+
throw new Error(`Telegram admission ${label} is invalid`);
|
|
92
|
+
}
|
|
93
|
+
function validateRecord(value, expectedId) {
|
|
94
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
95
|
+
throw new Error("Telegram admission record is invalid");
|
|
96
|
+
const record = value;
|
|
97
|
+
if (record.schemaVersion !== 1 || !ADMISSION_ID.test(record.id) || (expectedId !== undefined && record.id !== expectedId)
|
|
98
|
+
|| !Number.isSafeInteger(record.revision) || record.revision < 0
|
|
99
|
+
|| ![...ACTIVE_STATUSES, ...TERMINAL_STATUSES].includes(record.status)
|
|
100
|
+
|| typeof record.botId !== "string" || typeof record.userId !== "string" || typeof record.chatId !== "string"
|
|
101
|
+
|| !CANONICAL_ID.test(record.botId) || !CANONICAL_ID.test(record.userId) || !CANONICAL_ID.test(record.chatId)
|
|
102
|
+
|| !Number.isSafeInteger(record.updateId) || record.updateId < 0
|
|
103
|
+
|| !Number.isSafeInteger(record.messageId) || record.messageId < 1
|
|
104
|
+
|| (record.quarantinedText !== null && typeof record.quarantinedText !== "string")
|
|
105
|
+
|| typeof record.contentDigest !== "string" || !/^[a-f0-9]{64}$/u.test(record.contentDigest)
|
|
106
|
+
|| typeof record.displayLabel !== "string" || [...record.displayLabel].length > 120
|
|
107
|
+
|| typeof record.displayCode !== "string" || record.displayCode.length < 4 || record.displayCode.length > 32
|
|
108
|
+
|| typeof record.hasAttachments !== "boolean"
|
|
109
|
+
|| ![record.createdAt, record.updatedAt, record.expiresAt].every(Number.isSafeInteger)
|
|
110
|
+
|| (record.friendId !== null && (typeof record.friendId !== "string" || !record.friendId))
|
|
111
|
+
|| (record.acknowledgementArtifactId !== null && typeof record.acknowledgementArtifactId !== "string")
|
|
112
|
+
|| (record.ownerCardArtifactId !== null && typeof record.ownerCardArtifactId !== "string")
|
|
113
|
+
|| (record.ingressSessionKey !== null && (typeof record.ingressSessionKey !== "string" || record.ingressSessionKey.length < 1 || record.ingressSessionKey.length > 1_024))
|
|
114
|
+
|| (record.ingressEventId !== null && (typeof record.ingressEventId !== "string" || !/^evt-[0-9]{6,}$/u.test(record.ingressEventId)))
|
|
115
|
+
|| (record.ingressReference !== null && record.ingressReference !== `telegram-admission:${record.id}`)
|
|
116
|
+
|| ([record.ingressSessionKey, record.ingressEventId, record.ingressReference].filter((entry) => entry !== null).length !== 0
|
|
117
|
+
&& [record.ingressSessionKey, record.ingressEventId, record.ingressReference].some((entry) => entry === null))
|
|
118
|
+
|| (TERMINAL_STATUSES.has(record.status) && record.quarantinedText !== null)) {
|
|
119
|
+
throw new Error("Telegram admission record is invalid");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function atomicWrite(filePath, value) {
|
|
123
|
+
const temporaryPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
124
|
+
const handle = fs.openSync(temporaryPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o600);
|
|
125
|
+
try {
|
|
126
|
+
fs.writeFileSync(handle, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
127
|
+
fs.fsyncSync(handle);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
fs.closeSync(handle);
|
|
131
|
+
}
|
|
132
|
+
fs.renameSync(temporaryPath, filePath);
|
|
133
|
+
fs.chmodSync(filePath, 0o600);
|
|
134
|
+
const directory = fs.openSync(path.dirname(filePath), fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
|
|
135
|
+
try {
|
|
136
|
+
fs.fsyncSync(directory);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
fs.closeSync(directory);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function readBoundedPrivateJson(filePath, maxBytes, label) {
|
|
143
|
+
const handle = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
144
|
+
try {
|
|
145
|
+
const stat = fs.fstatSync(handle);
|
|
146
|
+
if (!stat.isFile() || stat.size < 2 || stat.size > maxBytes || (stat.mode & 0o777) !== 0o600) {
|
|
147
|
+
throw new Error(`Telegram admission ${label} is not a bounded private regular file`);
|
|
148
|
+
}
|
|
149
|
+
return JSON.parse(fs.readFileSync(handle, "utf8"));
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
fs.closeSync(handle);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
class FileTelegramAdmissionStore {
|
|
156
|
+
root;
|
|
157
|
+
now;
|
|
158
|
+
limits;
|
|
159
|
+
rootFd;
|
|
160
|
+
rootIdentity;
|
|
161
|
+
closed = false;
|
|
162
|
+
constructor(root, limits = {}, now = Date.now) {
|
|
163
|
+
this.root = root;
|
|
164
|
+
this.now = now;
|
|
165
|
+
this.limits = { ...DEFAULT_LIMITS, ...limits };
|
|
166
|
+
for (const [key, value] of Object.entries(this.limits)) {
|
|
167
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
168
|
+
throw new Error(`Telegram admission ${key} must be a positive safe integer`);
|
|
169
|
+
}
|
|
170
|
+
if (fs.existsSync(root)) {
|
|
171
|
+
const stat = fs.lstatSync(root);
|
|
172
|
+
if (stat.isSymbolicLink())
|
|
173
|
+
throw new Error("Telegram admission root must not be a symbolic link");
|
|
174
|
+
if (!stat.isDirectory())
|
|
175
|
+
throw new Error("Telegram admission root must be a directory");
|
|
176
|
+
}
|
|
177
|
+
else
|
|
178
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
179
|
+
fs.chmodSync(root, 0o700);
|
|
180
|
+
const stat = fs.lstatSync(root);
|
|
181
|
+
/* v8 ignore next -- race defense: constructor-created/chmodded roots satisfy this unless another process swaps the path between syscalls @preserve */
|
|
182
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o777) !== 0o700)
|
|
183
|
+
throw new Error("Telegram admission root is unsafe");
|
|
184
|
+
this.rootFd = fs.openSync(root, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
|
|
185
|
+
const pinned = fs.fstatSync(this.rootFd);
|
|
186
|
+
this.rootIdentity = { dev: pinned.dev, ino: pinned.ino };
|
|
187
|
+
}
|
|
188
|
+
assertRootIdentity() {
|
|
189
|
+
if (this.closed)
|
|
190
|
+
throw new Error("Telegram admission store is closed");
|
|
191
|
+
const pinned = fs.fstatSync(this.rootFd);
|
|
192
|
+
const current = fs.lstatSync(this.root);
|
|
193
|
+
if (!pinned.isDirectory() || !current.isDirectory() || current.isSymbolicLink()
|
|
194
|
+
|| pinned.dev !== this.rootIdentity.dev || pinned.ino !== this.rootIdentity.ino
|
|
195
|
+
|| current.dev !== this.rootIdentity.dev || current.ino !== this.rootIdentity.ino
|
|
196
|
+
|| (current.mode & 0o777) !== 0o700)
|
|
197
|
+
throw new Error("Telegram admission root identity changed");
|
|
198
|
+
}
|
|
199
|
+
close() {
|
|
200
|
+
if (this.closed)
|
|
201
|
+
return;
|
|
202
|
+
fs.closeSync(this.rootFd);
|
|
203
|
+
this.closed = true;
|
|
204
|
+
}
|
|
205
|
+
filePath(id) {
|
|
206
|
+
if (!ADMISSION_ID.test(id))
|
|
207
|
+
throw new Error("Telegram admission id is invalid");
|
|
208
|
+
return path.join(this.root, `${id}.json`);
|
|
209
|
+
}
|
|
210
|
+
coordinationPath(id) {
|
|
211
|
+
return `${this.filePath(id)}.decision`;
|
|
212
|
+
}
|
|
213
|
+
selfHealthPath() {
|
|
214
|
+
return path.join(this.root, "self-health.json");
|
|
215
|
+
}
|
|
216
|
+
rateStatePath() {
|
|
217
|
+
return path.join(this.root, "rate-window.json");
|
|
218
|
+
}
|
|
219
|
+
globalCoordinationPath() {
|
|
220
|
+
return path.join(path.dirname(this.root), `.${path.basename(this.root)}-coordination`, "admission");
|
|
221
|
+
}
|
|
222
|
+
readRateState(now) {
|
|
223
|
+
let state = { version: 1, events: [] };
|
|
224
|
+
try {
|
|
225
|
+
const value = readBoundedPrivateJson(this.rateStatePath(), 256 * 1024, "rate state");
|
|
226
|
+
if (value.version !== 1 || !Array.isArray(value.events) || value.events.some((event) => !event
|
|
227
|
+
|| !/^[a-f0-9]{64}$/u.test(event.identityDigest) || !/^[a-f0-9]{64}$/u.test(event.updateDigest)
|
|
228
|
+
|| !Number.isSafeInteger(event.observedAt) || event.observedAt < 0))
|
|
229
|
+
throw new Error("invalid");
|
|
230
|
+
state = value;
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (error.code !== "ENOENT")
|
|
234
|
+
throw new Error("Telegram admission rate state is invalid", { cause: error });
|
|
235
|
+
}
|
|
236
|
+
state.events = state.events.filter((event) => event.observedAt > now - this.limits.rateWindowMs);
|
|
237
|
+
return state;
|
|
238
|
+
}
|
|
239
|
+
recordRateAttempt(input, now) {
|
|
240
|
+
this.assertRootIdentity();
|
|
241
|
+
const state = this.readRateState(now);
|
|
242
|
+
const identityDigest = digest(`identity\0${identity(input)}`);
|
|
243
|
+
const updateDigest = digest(`update\0${input.botId}\0${input.updateId}`);
|
|
244
|
+
if (!state.events.some((event) => event.updateDigest === updateDigest))
|
|
245
|
+
state.events.push({ identityDigest, updateDigest, observedAt: now });
|
|
246
|
+
const identityCount = state.events.filter((event) => event.identityDigest === identityDigest).length;
|
|
247
|
+
const overflow = identityCount > this.limits.maxMessagesPerIdentity || state.events.length > this.limits.maxMessagesPerWindow;
|
|
248
|
+
state.events = state.events
|
|
249
|
+
.sort((left, right) => left.observedAt - right.observedAt || left.updateDigest.localeCompare(right.updateDigest))
|
|
250
|
+
.slice(-(this.limits.maxMessagesPerWindow + 1));
|
|
251
|
+
atomicWrite(this.rateStatePath(), state);
|
|
252
|
+
this.assertRootIdentity();
|
|
253
|
+
return overflow;
|
|
254
|
+
}
|
|
255
|
+
compactTerminalRecords(records, now) {
|
|
256
|
+
const terminal = records.filter((record) => TERMINAL_STATUSES.has(record.status))
|
|
257
|
+
.sort((left, right) => right.updatedAt - left.updatedAt || left.id.localeCompare(right.id));
|
|
258
|
+
const retained = new Set(terminal
|
|
259
|
+
.filter((record) => record.updatedAt > now - this.limits.terminalRetentionMs)
|
|
260
|
+
.slice(0, this.limits.maxTerminalRecords)
|
|
261
|
+
.map((record) => record.id));
|
|
262
|
+
for (const record of terminal) {
|
|
263
|
+
if (!retained.has(record.id))
|
|
264
|
+
fs.unlinkSync(this.filePath(record.id));
|
|
265
|
+
}
|
|
266
|
+
this.assertRootIdentity();
|
|
267
|
+
return records.filter((record) => !TERMINAL_STATUSES.has(record.status) || retained.has(record.id));
|
|
268
|
+
}
|
|
269
|
+
read(id) {
|
|
270
|
+
this.assertRootIdentity();
|
|
271
|
+
const filePath = this.filePath(id);
|
|
272
|
+
const handle = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
273
|
+
try {
|
|
274
|
+
const stat = fs.fstatSync(handle);
|
|
275
|
+
if (!stat.isFile() || stat.size < 2 || stat.size > this.limits.maxTextBytes + 8 * 1024)
|
|
276
|
+
throw new Error("Telegram admission record is not bounded");
|
|
277
|
+
const parsed = JSON.parse(fs.readFileSync(handle, "utf8"));
|
|
278
|
+
validateRecord(parsed, id);
|
|
279
|
+
this.assertRootIdentity();
|
|
280
|
+
return parsed;
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
fs.closeSync(handle);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
list() {
|
|
287
|
+
this.assertRootIdentity();
|
|
288
|
+
const records = fs.readdirSync(this.root, { withFileTypes: true })
|
|
289
|
+
.filter((entry) => entry.isFile() && /^[a-f0-9]{20}\.json$/u.test(entry.name))
|
|
290
|
+
.map((entry) => this.read(entry.name.slice(0, -5)));
|
|
291
|
+
this.assertRootIdentity();
|
|
292
|
+
return records;
|
|
293
|
+
}
|
|
294
|
+
write(record) {
|
|
295
|
+
this.assertRootIdentity();
|
|
296
|
+
validateRecord(record, record.id);
|
|
297
|
+
atomicWrite(this.filePath(record.id), record);
|
|
298
|
+
this.assertRootIdentity();
|
|
299
|
+
}
|
|
300
|
+
readSelfHealth() {
|
|
301
|
+
this.assertRootIdentity();
|
|
302
|
+
try {
|
|
303
|
+
const value = readBoundedPrivateJson(this.selfHealthPath(), 4 * 1024, "self-health");
|
|
304
|
+
if (value.schemaVersion !== 1 || value.code !== "telegram_admission_overflow" || !Number.isSafeInteger(value.count)
|
|
305
|
+
|| value.count < 1 || !Number.isSafeInteger(value.lastObservedAt))
|
|
306
|
+
throw new Error("invalid");
|
|
307
|
+
this.assertRootIdentity();
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
if (error.code === "ENOENT")
|
|
312
|
+
return null;
|
|
313
|
+
throw new Error("Telegram admission self-health is invalid", { cause: error });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
recordOverflow() {
|
|
317
|
+
const now = this.now();
|
|
318
|
+
const current = this.readSelfHealth();
|
|
319
|
+
atomicWrite(this.selfHealthPath(), {
|
|
320
|
+
schemaVersion: 1,
|
|
321
|
+
code: "telegram_admission_overflow",
|
|
322
|
+
count: (current?.count ?? 0) + 1,
|
|
323
|
+
lastObservedAt: now,
|
|
324
|
+
});
|
|
325
|
+
(0, runtime_1.emitNervesEvent)({
|
|
326
|
+
level: "warn",
|
|
327
|
+
component: "senses",
|
|
328
|
+
event: "senses.telegram_admission_overflow",
|
|
329
|
+
message: "Telegram admission capacity was exceeded",
|
|
330
|
+
meta: { count: (current?.count ?? 0) + 1 },
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
capture(input, displayCode) {
|
|
334
|
+
return (0, session_transaction_1.withImmediateSessionTurnLease)(this.globalCoordinationPath(), () => this.captureExclusive(input, displayCode));
|
|
335
|
+
}
|
|
336
|
+
captureExclusive(input, displayCode) {
|
|
337
|
+
assertSafeInteger(input.updateId, "update id");
|
|
338
|
+
assertSafeInteger(input.messageId, "message id");
|
|
339
|
+
if (input.messageId < 1)
|
|
340
|
+
throw new Error("Telegram admission message id is invalid");
|
|
341
|
+
assertCanonicalId(input.botId, "bot id");
|
|
342
|
+
assertCanonicalId(input.userId, "user id");
|
|
343
|
+
assertCanonicalId(input.chatId, "chat id");
|
|
344
|
+
const now = this.now();
|
|
345
|
+
const records = this.compactTerminalRecords(this.list(), now);
|
|
346
|
+
if (this.recordRateAttempt(input, now)) {
|
|
347
|
+
this.recordOverflow();
|
|
348
|
+
return { kind: "overflow" };
|
|
349
|
+
}
|
|
350
|
+
const sameIdentity = records.filter((record) => identity(record) === identity(input))
|
|
351
|
+
.sort((left, right) => right.createdAt - left.createdAt);
|
|
352
|
+
const blocked = sameIdentity.find((record) => record.status === "blocked");
|
|
353
|
+
if (blocked)
|
|
354
|
+
return { kind: "blocked" };
|
|
355
|
+
const active = sameIdentity.find((record) => ACTIVE_STATUSES.has(record.status));
|
|
356
|
+
if (active)
|
|
357
|
+
return { kind: "existing", record: active };
|
|
358
|
+
const recentTerminal = sameIdentity[0];
|
|
359
|
+
if (recentTerminal && now - recentTerminal.updatedAt < this.limits.retryCooldownMs)
|
|
360
|
+
return { kind: "blocked" };
|
|
361
|
+
const textBytes = Buffer.byteLength(input.text, "utf8");
|
|
362
|
+
const pending = records.filter((record) => ACTIVE_STATUSES.has(record.status));
|
|
363
|
+
const pendingBytes = pending.reduce((total, record) => total + Buffer.byteLength(
|
|
364
|
+
/* v8 ignore next -- active records are validated to retain quarantined text; fallback is corruption defense @preserve */
|
|
365
|
+
record.quarantinedText ?? "", "utf8"), 0);
|
|
366
|
+
if (textBytes > this.limits.maxTextBytes || pending.length >= this.limits.maxPendingContacts
|
|
367
|
+
|| pendingBytes + textBytes > this.limits.maxTotalBytes || this.limits.maxMessagesPerIdentity < 1) {
|
|
368
|
+
this.recordOverflow();
|
|
369
|
+
return { kind: "overflow" };
|
|
370
|
+
}
|
|
371
|
+
const id = admissionId(input);
|
|
372
|
+
const replay = records.find((record) => record.id === id);
|
|
373
|
+
/* v8 ignore next -- active same-identity records return above; an ID replay reaching here is necessarily terminal @preserve */
|
|
374
|
+
if (replay)
|
|
375
|
+
return ACTIVE_STATUSES.has(replay.status) ? { kind: "existing", record: replay } : { kind: "blocked" };
|
|
376
|
+
const record = {
|
|
377
|
+
schemaVersion: 1,
|
|
378
|
+
id,
|
|
379
|
+
revision: 0,
|
|
380
|
+
status: "pending",
|
|
381
|
+
botId: input.botId,
|
|
382
|
+
userId: input.userId,
|
|
383
|
+
chatId: input.chatId,
|
|
384
|
+
updateId: input.updateId,
|
|
385
|
+
messageId: input.messageId,
|
|
386
|
+
quarantinedText: input.text,
|
|
387
|
+
contentDigest: digest(input.text),
|
|
388
|
+
displayLabel: boundedLabel(input.displayLabel),
|
|
389
|
+
displayCode,
|
|
390
|
+
hasAttachments: input.hasAttachments,
|
|
391
|
+
createdAt: now,
|
|
392
|
+
updatedAt: now,
|
|
393
|
+
expiresAt: now + this.limits.retentionMs,
|
|
394
|
+
friendId: null,
|
|
395
|
+
acknowledgementArtifactId: null,
|
|
396
|
+
ownerCardArtifactId: null,
|
|
397
|
+
ingressSessionKey: null,
|
|
398
|
+
ingressEventId: null,
|
|
399
|
+
ingressReference: null,
|
|
400
|
+
};
|
|
401
|
+
this.write(record);
|
|
402
|
+
(0, runtime_1.emitNervesEvent)({
|
|
403
|
+
component: "senses",
|
|
404
|
+
event: "senses.telegram_admission_captured",
|
|
405
|
+
message: "captured an unknown Telegram contact before friend resolution",
|
|
406
|
+
meta: { admissionId: id, hasAttachments: input.hasAttachments, textBytes },
|
|
407
|
+
});
|
|
408
|
+
return { kind: "created", record };
|
|
409
|
+
}
|
|
410
|
+
compareAndSwap(input) {
|
|
411
|
+
const current = this.read(input.admissionId);
|
|
412
|
+
if (current.status !== input.expectedStatus)
|
|
413
|
+
throw new Error(`Telegram admission CAS failed: expected ${input.expectedStatus}, found ${current.status}`);
|
|
414
|
+
const now = this.now();
|
|
415
|
+
const terminal = TERMINAL_STATUSES.has(input.nextStatus);
|
|
416
|
+
const updated = {
|
|
417
|
+
...current,
|
|
418
|
+
status: input.nextStatus,
|
|
419
|
+
revision: current.revision + 1,
|
|
420
|
+
updatedAt: now,
|
|
421
|
+
...(input.friendId ? { friendId: input.friendId } : {}),
|
|
422
|
+
...(input.acknowledgementArtifactId ? { acknowledgementArtifactId: input.acknowledgementArtifactId } : {}),
|
|
423
|
+
...(input.ownerCardArtifactId ? { ownerCardArtifactId: input.ownerCardArtifactId } : {}),
|
|
424
|
+
...(input.ingress ? { ingressSessionKey: input.ingress.sessionKey, ingressEventId: input.ingress.eventId, ingressReference: input.ingress.reference } : {}),
|
|
425
|
+
...(terminal || input.ingress ? { quarantinedText: null } : {}),
|
|
426
|
+
};
|
|
427
|
+
this.write(updated);
|
|
428
|
+
return updated;
|
|
429
|
+
}
|
|
430
|
+
recordEffect(admissionIdValue, kind, artifactId) {
|
|
431
|
+
const current = this.read(admissionIdValue);
|
|
432
|
+
if (current.status !== "pending")
|
|
433
|
+
return current;
|
|
434
|
+
const updated = {
|
|
435
|
+
...current,
|
|
436
|
+
revision: current.revision + 1,
|
|
437
|
+
updatedAt: this.now(),
|
|
438
|
+
...(kind === "acknowledgement" ? { acknowledgementArtifactId: artifactId } : { ownerCardArtifactId: artifactId }),
|
|
439
|
+
};
|
|
440
|
+
this.write(updated);
|
|
441
|
+
return updated;
|
|
442
|
+
}
|
|
443
|
+
releaseBlock(admissionIdValue) {
|
|
444
|
+
return (0, session_transaction_1.withImmediateSessionTurnLease)(this.globalCoordinationPath(), () => {
|
|
445
|
+
const current = this.read(admissionIdValue);
|
|
446
|
+
if (current.status !== "blocked")
|
|
447
|
+
throw new Error("Telegram admission is not blocked");
|
|
448
|
+
return this.compareAndSwap({ admissionId: current.id, expectedStatus: "blocked", nextStatus: "denied" });
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
exports.FileTelegramAdmissionStore = FileTelegramAdmissionStore;
|
|
453
|
+
function ownerCard(record) {
|
|
454
|
+
return [
|
|
455
|
+
"Unknown Telegram contact",
|
|
456
|
+
`Unverified Telegram label: ${(0, telegram_client_1.escapeTelegramHtml)(record.displayLabel).replaceAll("'", "'")}`,
|
|
457
|
+
`Verification code: ${(0, telegram_client_1.escapeTelegramHtml)(record.displayCode)}`,
|
|
458
|
+
"The message is quarantined and has not been processed.",
|
|
459
|
+
record.hasAttachments ? "Attachments were not downloaded and must be resent after approval." : null,
|
|
460
|
+
].filter((line) => line !== null).join("\n");
|
|
461
|
+
}
|
|
462
|
+
function createTelegramAdmissionController(options) {
|
|
463
|
+
const now = options.now ?? Date.now;
|
|
464
|
+
const createDisplayCode = options.createDisplayCode ?? (() => crypto.randomBytes(4).toString("hex").toUpperCase());
|
|
465
|
+
const withDecisionLease = options.withDecisionLease ?? (async (_admissionId, work) => work());
|
|
466
|
+
const settleOwnerCard = async (record) => {
|
|
467
|
+
const text = record.status === "handled" ? "Allowed"
|
|
468
|
+
: record.status === "denied" ? "Denied"
|
|
469
|
+
: record.status === "blocked" ? "Blocked"
|
|
470
|
+
: record.status === "expired" ? "Expired"
|
|
471
|
+
: null;
|
|
472
|
+
if (!text || !record.ownerCardArtifactId)
|
|
473
|
+
return;
|
|
474
|
+
const messageId = options.resolveOwnerCardMessageId?.(record.ownerCardArtifactId);
|
|
475
|
+
if (!messageId)
|
|
476
|
+
return;
|
|
477
|
+
await options.sendEffect({
|
|
478
|
+
idempotencyKey: `owner-card-terminal:${record.id}:${record.status}`,
|
|
479
|
+
target: { kind: "approved_relationship", ...options.owner, requestId: record.id },
|
|
480
|
+
authorClass: "control",
|
|
481
|
+
effect: { kind: "edit", messageId, text },
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
const ensureEffects = async (initial) => {
|
|
485
|
+
let record = options.store.read(initial.id);
|
|
486
|
+
if (!record.acknowledgementArtifactId) {
|
|
487
|
+
const artifactIdValue = await options.sendEffect({
|
|
488
|
+
idempotencyKey: `ack:${record.id}`,
|
|
489
|
+
target: { kind: "admission_gate", admissionId: record.id, botId: record.botId, userId: record.userId, chatId: record.chatId },
|
|
490
|
+
authorClass: "control",
|
|
491
|
+
effect: { kind: "admission_ack", text: telegram_effect_adapter_1.FIXED_ADMISSION_ACKNOWLEDGEMENT },
|
|
492
|
+
});
|
|
493
|
+
record = options.store.recordEffect(record.id, "acknowledgement", artifactIdValue);
|
|
494
|
+
}
|
|
495
|
+
if (!record.ownerCardArtifactId) {
|
|
496
|
+
const artifactIdValue = await options.sendEffect({
|
|
497
|
+
idempotencyKey: `owner-card:${record.id}`,
|
|
498
|
+
target: { kind: "approved_relationship", ...options.owner, requestId: record.id },
|
|
499
|
+
authorClass: "control",
|
|
500
|
+
effect: { kind: "card", text: ownerCard(record), buttons: [[
|
|
501
|
+
{ text: "Allow", callbackData: `admit:${record.id}:allow` },
|
|
502
|
+
{ text: "Deny", callbackData: `admit:${record.id}:deny` },
|
|
503
|
+
{ text: "Block", callbackData: `admit:${record.id}:block` },
|
|
504
|
+
]] },
|
|
505
|
+
});
|
|
506
|
+
record = options.store.recordEffect(record.id, "owner_card", artifactIdValue);
|
|
507
|
+
}
|
|
508
|
+
return record;
|
|
509
|
+
};
|
|
510
|
+
const compensateFriend = async (record) => {
|
|
511
|
+
if (!record.friendId)
|
|
512
|
+
return;
|
|
513
|
+
const revocation = await options.revokeFriend({
|
|
514
|
+
provider: "telegram-user",
|
|
515
|
+
botId: record.botId,
|
|
516
|
+
userId: record.userId,
|
|
517
|
+
chatId: record.chatId,
|
|
518
|
+
admissionId: record.id,
|
|
519
|
+
friendId: record.friendId,
|
|
520
|
+
});
|
|
521
|
+
if (revocation.kind === "collision")
|
|
522
|
+
throw new Error(`Telegram admission compensation collision: ${revocation.reason}`);
|
|
523
|
+
};
|
|
524
|
+
const resume = async (admissionIdValue) => {
|
|
525
|
+
let record = options.store.read(admissionIdValue);
|
|
526
|
+
if (record.status === "approved") {
|
|
527
|
+
const claim = await options.claimFriend({
|
|
528
|
+
provider: "telegram-user",
|
|
529
|
+
botId: record.botId,
|
|
530
|
+
userId: record.userId,
|
|
531
|
+
chatId: record.chatId,
|
|
532
|
+
admissionId: record.id,
|
|
533
|
+
displayLabel: record.displayLabel,
|
|
534
|
+
defaults: {
|
|
535
|
+
trustLevel: "friend",
|
|
536
|
+
admissionState: "active",
|
|
537
|
+
initiativePolicy: "request_follow_up_only",
|
|
538
|
+
capabilityProfileId: "sanctuary-household",
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
if (claim.kind === "collision") {
|
|
542
|
+
options.store.compareAndSwap({ admissionId: record.id, expectedStatus: "approved", nextStatus: "collision" });
|
|
543
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.telegram_admission_collision", message: "Telegram admission Friend claim collided", meta: { admissionId: record.id } });
|
|
544
|
+
throw new Error(`Telegram admission collision: ${claim.reason}`);
|
|
545
|
+
}
|
|
546
|
+
record = options.store.compareAndSwap({ admissionId: record.id, expectedStatus: "approved", nextStatus: "friend_bound", friendId: claim.friendId });
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
if (record.status === "friend_bound") {
|
|
550
|
+
if (record.quarantinedText === null || !record.friendId)
|
|
551
|
+
throw new Error("Telegram admission lost input before durable ingress");
|
|
552
|
+
const committedBase = await options.commitApprovedIngress({
|
|
553
|
+
admissionId: record.id,
|
|
554
|
+
idempotencyKey: `admission-turn:${record.id}`,
|
|
555
|
+
friendId: record.friendId,
|
|
556
|
+
botId: record.botId,
|
|
557
|
+
userId: record.userId,
|
|
558
|
+
chatId: record.chatId,
|
|
559
|
+
updateId: record.updateId,
|
|
560
|
+
messageId: record.messageId,
|
|
561
|
+
text: record.quarantinedText,
|
|
562
|
+
hasAttachments: record.hasAttachments,
|
|
563
|
+
synthetic: false,
|
|
564
|
+
});
|
|
565
|
+
const committed = { ...committedBase, orientation: exports.NEWLY_ADMITTED_ORIENTATION };
|
|
566
|
+
if (committed.admissionId !== record.id || committed.friendId !== record.friendId || committed.reference !== `telegram-admission:${record.id}`) {
|
|
567
|
+
throw new Error("Telegram admission committed ingress changed identity");
|
|
568
|
+
}
|
|
569
|
+
await options.enqueueApprovedWork(committedBase);
|
|
570
|
+
record = options.store.compareAndSwap({ admissionId: record.id, expectedStatus: "friend_bound", nextStatus: "ingress_committed", ingress: committed });
|
|
571
|
+
}
|
|
572
|
+
if (record.status === "ingress_committed") {
|
|
573
|
+
record = options.store.compareAndSwap({ admissionId: record.id, expectedStatus: "ingress_committed", nextStatus: "turn_queued" });
|
|
574
|
+
}
|
|
575
|
+
if (!record.friendId || !record.ingressSessionKey || !record.ingressEventId || !record.ingressReference)
|
|
576
|
+
throw new Error("Telegram admission queued work lost its durable reference");
|
|
577
|
+
const settlement = await options.dispatchApprovedWork({ admissionId: record.id, friendId: record.friendId, sessionKey: record.ingressSessionKey, eventId: record.ingressEventId, reference: record.ingressReference, orientation: exports.NEWLY_ADMITTED_ORIENTATION });
|
|
578
|
+
if (settlement === "indeterminate") {
|
|
579
|
+
await compensateFriend(record);
|
|
580
|
+
return options.store.compareAndSwap({ admissionId: record.id, expectedStatus: "turn_queued", nextStatus: "indeterminate" });
|
|
581
|
+
}
|
|
582
|
+
record = options.store.compareAndSwap({ admissionId: record.id, expectedStatus: "turn_queued", nextStatus: "handled" });
|
|
583
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.telegram_admission_handled", message: "Telegram admission dispatched one approved user turn", meta: { admissionId: record.id, friendId: record.friendId } });
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
const failed = options.store.read(record.id);
|
|
587
|
+
if (failed.friendId && !TERMINAL_STATUSES.has(failed.status)) {
|
|
588
|
+
await compensateFriend(failed);
|
|
589
|
+
options.store.compareAndSwap({ admissionId: failed.id, expectedStatus: failed.status, nextStatus: "indeterminate" });
|
|
590
|
+
}
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
return record;
|
|
594
|
+
};
|
|
595
|
+
const reconcileExpired = async () => {
|
|
596
|
+
for (const candidate of options.store.list()) {
|
|
597
|
+
await withDecisionLease(candidate.id, async () => {
|
|
598
|
+
const record = options.store.read(candidate.id);
|
|
599
|
+
if (ACTIVE_STATUSES.has(record.status) && now() >= record.expiresAt) {
|
|
600
|
+
await compensateFriend(record);
|
|
601
|
+
const expired = options.store.compareAndSwap({ admissionId: record.id, expectedStatus: record.status, nextStatus: "expired" });
|
|
602
|
+
await settleOwnerCard(expired);
|
|
603
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.telegram_admission_expired", message: "Telegram admission expired and raw content was purged", meta: { admissionId: record.id } });
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
return {
|
|
609
|
+
async handleUnknown(input) {
|
|
610
|
+
const captured = options.store.capture(input, createDisplayCode());
|
|
611
|
+
if (!("record" in captured))
|
|
612
|
+
return { kind: captured.kind };
|
|
613
|
+
const record = await ensureEffects(captured.record);
|
|
614
|
+
return { kind: "pending", admissionId: record.id };
|
|
615
|
+
},
|
|
616
|
+
parseCallback(value, messageId) {
|
|
617
|
+
const match = /^admit:([a-f0-9]{20}):(allow|deny|block)$/u.exec(value);
|
|
618
|
+
if (!match)
|
|
619
|
+
throw new Error("Telegram admission callback is invalid");
|
|
620
|
+
const record = options.store.read(match[1]);
|
|
621
|
+
const binding = options.resolveOwnerCard(messageId);
|
|
622
|
+
if (!binding || binding.admissionId !== record.id || binding.artifactId !== record.ownerCardArtifactId)
|
|
623
|
+
throw new Error("Telegram admission callback is not bound to its owner card");
|
|
624
|
+
return { admissionId: match[1], decision: match[2] };
|
|
625
|
+
},
|
|
626
|
+
parseOwnerDecision(input) {
|
|
627
|
+
if (!input.replyToMessageId || Buffer.byteLength(input.text, "utf8") > 80)
|
|
628
|
+
return null;
|
|
629
|
+
const normalized = input.text.normalize("NFKC").trim().toLocaleLowerCase("en-US");
|
|
630
|
+
const decision = /^(?:yes|allow|approve|let (?:them|him|her) in)$/u.test(normalized) ? "allow"
|
|
631
|
+
: /^(?:no|deny|decline)$/u.test(normalized) ? "deny"
|
|
632
|
+
: /^(?:block|block (?:them|him|her))$/u.test(normalized) ? "block"
|
|
633
|
+
: null;
|
|
634
|
+
if (!decision)
|
|
635
|
+
return null;
|
|
636
|
+
const binding = options.resolveOwnerCard(input.replyToMessageId);
|
|
637
|
+
if (!binding)
|
|
638
|
+
return null;
|
|
639
|
+
const record = options.store.read(binding.admissionId);
|
|
640
|
+
if (record.ownerCardArtifactId !== binding.artifactId || record.status !== "pending")
|
|
641
|
+
return null;
|
|
642
|
+
return { admissionId: record.id, decision };
|
|
643
|
+
},
|
|
644
|
+
async decide(input) {
|
|
645
|
+
return withDecisionLease(input.admissionId, async () => {
|
|
646
|
+
if (input.actorFriendId !== options.owner.friendId)
|
|
647
|
+
throw new Error("Only the Telegram admission owner may decide");
|
|
648
|
+
let record = options.store.read(input.admissionId);
|
|
649
|
+
if (TERMINAL_STATUSES.has(record.status))
|
|
650
|
+
return record;
|
|
651
|
+
if (record.status !== "pending")
|
|
652
|
+
return resume(record.id);
|
|
653
|
+
if (now() >= record.expiresAt) {
|
|
654
|
+
record = options.store.compareAndSwap({ admissionId: record.id, expectedStatus: "pending", nextStatus: "expired" });
|
|
655
|
+
await settleOwnerCard(record);
|
|
656
|
+
return record;
|
|
657
|
+
}
|
|
658
|
+
if (input.decision === "deny" || input.decision === "block") {
|
|
659
|
+
record = options.store.compareAndSwap({ admissionId: record.id, expectedStatus: "pending", nextStatus: input.decision === "deny" ? "denied" : "blocked" });
|
|
660
|
+
await settleOwnerCard(record);
|
|
661
|
+
return record;
|
|
662
|
+
}
|
|
663
|
+
record = options.store.compareAndSwap({ admissionId: record.id, expectedStatus: "pending", nextStatus: "approved" });
|
|
664
|
+
record = await resume(record.id);
|
|
665
|
+
await settleOwnerCard(record);
|
|
666
|
+
return record;
|
|
667
|
+
});
|
|
668
|
+
},
|
|
669
|
+
async reconcileExpired() { await reconcileExpired(); },
|
|
670
|
+
async recover() {
|
|
671
|
+
await reconcileExpired();
|
|
672
|
+
for (const record of options.store.list()) {
|
|
673
|
+
if (record.status === "pending")
|
|
674
|
+
await ensureEffects(record);
|
|
675
|
+
else if (["approved", "friend_bound", "ingress_committed", "turn_queued"].includes(record.status)) {
|
|
676
|
+
await withDecisionLease(record.id, async () => settleOwnerCard(await resume(record.id)));
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
await withDecisionLease(record.id, () => settleOwnerCard(record));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
}
|