@ouro.bot/cli 0.1.0-alpha.815 → 0.1.0-alpha.817

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.
Files changed (63) hide show
  1. package/assets/sanctuary-host-launcher.sh +16 -0
  2. package/changelog.json +18 -0
  3. package/deploy/unraid/Dockerfile +6 -0
  4. package/deploy/unraid/README.txt +305 -365
  5. package/deploy/unraid/docker-man-template-transaction.mjs +192 -7
  6. package/deploy/unraid/sanctuary-acceptance-adapter.sh +1 -1
  7. package/deploy/unraid/sanctuary-acceptance-contract.json +7 -7
  8. package/deploy/unraid/sanctuary-authority-installation.json +36 -0
  9. package/deploy/unraid/sanctuary-authority-service.sh +30 -0
  10. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +62 -9
  11. package/deploy/unraid/sanctuary-unit16-run.sh +15 -15
  12. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  13. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
  14. package/deploy/unraid/sanctuary.xml +2 -1
  15. package/dist/heart/approval-store.js +11 -1
  16. package/dist/heart/core.js +2 -1
  17. package/dist/heart/daemon/container-spec-auditor-main.js +9 -8
  18. package/dist/heart/daemon/container-spec-auditor.js +17 -24
  19. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +95 -91
  20. package/dist/heart/daemon/sanctuary-acceptance-harness.js +95 -169
  21. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +4 -5
  22. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  23. package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
  24. package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
  25. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  26. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +780 -0
  27. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  28. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  29. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
  30. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  31. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  32. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  33. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  34. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
  35. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  36. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +615 -0
  37. package/dist/heart/daemon/sense-manager.js +20 -10
  38. package/dist/heart/external-events/router.js +31 -15
  39. package/dist/heart/steward-policy.js +374 -58
  40. package/dist/heart/tool-approval.js +8 -1
  41. package/dist/repertoire/relationship-authorization.js +128 -0
  42. package/dist/repertoire/tools-base.js +9 -13
  43. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  44. package/dist/repertoire/tools-steward-policy.js +34 -10
  45. package/dist/repertoire/tools-unraid.js +79 -32
  46. package/dist/repertoire/tools.js +37 -25
  47. package/dist/repertoire/unraid-restart.js +179 -52
  48. package/dist/senses/private-runtime.js +27 -12
  49. package/dist/senses/root-host-approval-port.js +345 -0
  50. package/dist/senses/root-host-approval-runtime.js +393 -0
  51. package/dist/senses/sanctuary-authority-resident.js +102 -0
  52. package/dist/senses/sanctuary-health-runner.js +0 -1
  53. package/dist/senses/sanctuary-media-catalog-contract.js +4 -1
  54. package/dist/senses/sanctuary-runtime.js +2 -0
  55. package/dist/senses/telegram-admission.js +7 -0
  56. package/dist/senses/telegram-approval-runtime.js +130 -12
  57. package/dist/senses/telegram-attachments.js +5 -1
  58. package/dist/senses/telegram-authority-transport.js +231 -0
  59. package/dist/senses/telegram-client.js +31 -8
  60. package/dist/senses/telegram-entry.js +1 -1
  61. package/dist/senses/telegram.js +174 -28
  62. package/npm-shrinkwrap.json +2 -2
  63. package/package.json +1 -1
@@ -0,0 +1,1008 @@
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.FileSanctuaryHostAuthority = void 0;
37
+ exports.hasHostInlineCodeSwitch = hasHostInlineCodeSwitch;
38
+ exports.validateHostProposalRequest = validateHostProposalRequest;
39
+ exports.hostProposalCommitment = hostProposalCommitment;
40
+ exports.sanctuaryHostAuthorityStatePath = sanctuaryHostAuthorityStatePath;
41
+ const node_crypto_1 = require("node:crypto");
42
+ const fs = __importStar(require("node:fs"));
43
+ const path = __importStar(require("node:path"));
44
+ const runtime_1 = require("../../nerves/runtime");
45
+ const session_transaction_1 = require("../../mind/session-transaction");
46
+ const sanctuary_authority_codec_1 = require("./sanctuary-authority-codec");
47
+ const SCHEMA_VERSION = 1;
48
+ const PROPOSAL_DOMAIN = "ouro.sanctuary.host-proposal.v1";
49
+ const REGISTRATION_DOMAIN = "ouro.sanctuary.host-registration.v1";
50
+ const DECISION_DOMAIN = "ouro.sanctuary.host-decision.v1";
51
+ const PERMIT_DOMAIN = "ouro.sanctuary.host-permit.v1";
52
+ const RECEIPT_DOMAIN = "ouro.sanctuary.host-receipt.v1";
53
+ const RETIREMENT_DOMAIN = "ouro.sanctuary.host-retirement.v1";
54
+ const ROOT_HOST_PREFIX = "<b>OURO ROOT HOST APPROVAL</b>";
55
+ const APPROVAL_TTL_MS = 300_000;
56
+ const MAX_TIMEOUT_MS = 900_000;
57
+ const MAX_SCRIPT_BYTES = 2_048;
58
+ const MAX_ARGUMENTS = 64;
59
+ const MAX_ARGUMENT_BYTES = 512;
60
+ const PRINTABLE_ASCII = /^[\x20-\x7e]+$/u;
61
+ const SCRIPT_BYTES = /^[\x0a\x20-\x7e]+$/u;
62
+ const DIGEST = /^sha256:[a-f0-9]{64}$/u;
63
+ const DECIMAL_ID = /^[1-9][0-9]*$/u;
64
+ const REGISTRATION_ID = /^hostreg-[A-Za-z0-9_-]{43}$/u;
65
+ const CALLBACK_ID = /^[\x20-\x7e]{1,256}$/u;
66
+ const INLINE_CODE_SWITCHES = new Set(["-c", "-e", "--eval", "--evaluate"]);
67
+ const CARD_STATUS = {
68
+ committed: "Awaiting owner decision.",
69
+ approved: "Approved. Root execution is pending.",
70
+ denied: "Denied. This approval is consumed and cannot be reused.",
71
+ expired: "Expired. This approval is consumed and cannot be reused.",
72
+ executed_verified: "Execution completed and verification succeeded.",
73
+ executed_failed: "Execution failed or was interrupted.",
74
+ executed_ambiguous: "Execution finished, but verification is ambiguous.",
75
+ retired: "Authority retired before execution. This permit is refused.",
76
+ };
77
+ function isObject(value) {
78
+ return typeof value === "object" && value !== null && !Array.isArray(value);
79
+ }
80
+ function exactKeys(value, keys) {
81
+ const actual = Object.keys(value).sort();
82
+ const expected = [...keys].sort();
83
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
84
+ }
85
+ function validTime(value) {
86
+ if (typeof value !== "string")
87
+ return false;
88
+ const instant = new Date(value);
89
+ return !Number.isNaN(instant.getTime()) && instant.toISOString() === value;
90
+ }
91
+ function printable(value, maximum) {
92
+ return typeof value === "string" && value.length > 0 && value.length <= maximum && PRINTABLE_ASCII.test(value);
93
+ }
94
+ function absoluteExecutable(value) {
95
+ return printable(value, 512)
96
+ && path.posix.isAbsolute(value)
97
+ && path.posix.normalize(value) === value
98
+ && !value.includes("/../")
99
+ && !value.endsWith("/..");
100
+ }
101
+ function validArguments(value) {
102
+ return Array.isArray(value)
103
+ && value.length <= MAX_ARGUMENTS
104
+ && value.every((argument) => printable(argument, MAX_ARGUMENT_BYTES));
105
+ }
106
+ function hasHostInlineCodeSwitch(arguments_, program) {
107
+ const name = typeof program === "string" ? path.posix.basename(program) : "";
108
+ return arguments_.some((argument) => INLINE_CODE_SWITCHES.has(argument)
109
+ || [...INLINE_CODE_SWITCHES].some((flag) => argument.startsWith(`${flag}=`))
110
+ || /^-[A-Za-z]*c/u.test(argument)
111
+ || (/^(node|nodejs)$/u.test(name) && (/^-[A-Za-z]*[ep]/u.test(argument) || /^--print(?:=|$)/u.test(argument)))
112
+ || (name === "perl" && /^-[A-Za-z]*[eE]/u.test(argument))
113
+ || (name === "ruby" && /^-[A-Za-z]*e/u.test(argument)));
114
+ }
115
+ function validateCommand(value) {
116
+ if (!isObject(value) || typeof value.kind !== "string")
117
+ throw new Error("Sanctuary host command is invalid");
118
+ if (value.kind === "executable") {
119
+ if (!exactKeys(value, ["kind", "executable", "arguments"])
120
+ || !absoluteExecutable(value.executable)
121
+ || !validArguments(value.arguments)
122
+ || hasHostInlineCodeSwitch(value.arguments, value.executable)) {
123
+ throw new Error("Sanctuary host executable command is invalid");
124
+ }
125
+ return;
126
+ }
127
+ if (value.kind !== "script"
128
+ || !exactKeys(value, ["kind", "interpreter", "arguments", "script"])
129
+ || !absoluteExecutable(value.interpreter)
130
+ || !validArguments(value.arguments)
131
+ || hasHostInlineCodeSwitch(value.arguments, value.interpreter)
132
+ || typeof value.script !== "string"
133
+ || Buffer.byteLength(value.script, "utf8") === 0
134
+ || Buffer.byteLength(value.script, "utf8") > MAX_SCRIPT_BYTES
135
+ || !SCRIPT_BYTES.test(value.script)
136
+ || value.script.split("\n").some((line) => line.endsWith(" "))) {
137
+ throw new Error("Sanctuary host script command is invalid");
138
+ }
139
+ }
140
+ function validateHostProposalRequest(value, expectedHost) {
141
+ if (!isObject(value) || !exactKeys(value, [
142
+ "targetHost",
143
+ "targetResource",
144
+ "command",
145
+ "workingDirectoryProfile",
146
+ "environmentProfile",
147
+ "timeoutMs",
148
+ "verification",
149
+ "ownerObservation",
150
+ ])) {
151
+ throw new Error("Sanctuary host proposal is malformed");
152
+ }
153
+ if (value.targetHost !== expectedHost
154
+ || !printable(value.targetResource, 256)
155
+ || value.workingDirectoryProfile !== "host.root.v1"
156
+ || value.environmentProfile !== "host.clean.v1"
157
+ || !Number.isSafeInteger(value.timeoutMs)
158
+ || value.timeoutMs < 1_000
159
+ || value.timeoutMs > MAX_TIMEOUT_MS) {
160
+ throw new Error("Sanctuary host proposal fields are invalid");
161
+ }
162
+ validateCommand(value.command);
163
+ if (value.verification !== null) {
164
+ if (!isObject(value.verification)
165
+ || !exactKeys(value.verification, ["profile", "expectedStateDigest"])
166
+ || value.verification.profile !== "file.digest.v1"
167
+ || typeof value.verification.expectedStateDigest !== "string"
168
+ || !DIGEST.test(value.verification.expectedStateDigest)
169
+ || !absoluteExecutable(value.targetResource)) {
170
+ throw new Error("Sanctuary host verification is invalid");
171
+ }
172
+ }
173
+ if (!isObject(value.ownerObservation)
174
+ || !exactKeys(value.ownerObservation, ["digest", "updateId", "userId", "chatId", "messageId"])
175
+ || typeof value.ownerObservation.digest !== "string"
176
+ || !DIGEST.test(value.ownerObservation.digest)
177
+ || !Number.isSafeInteger(value.ownerObservation.updateId)
178
+ || value.ownerObservation.updateId < 0
179
+ || typeof value.ownerObservation.userId !== "string"
180
+ || !DECIMAL_ID.test(value.ownerObservation.userId)
181
+ || typeof value.ownerObservation.chatId !== "string"
182
+ || !DECIMAL_ID.test(value.ownerObservation.chatId)
183
+ || typeof value.ownerObservation.messageId !== "string"
184
+ || !DECIMAL_ID.test(value.ownerObservation.messageId)) {
185
+ throw new Error("Sanctuary host owner observation is invalid");
186
+ }
187
+ }
188
+ function html(value) {
189
+ return value
190
+ .replaceAll("&", "&amp;")
191
+ .replaceAll("<", "&lt;")
192
+ .replaceAll(">", "&gt;")
193
+ .replaceAll("\"", "&quot;")
194
+ .replaceAll("'", "&#39;");
195
+ }
196
+ function digestText(value) {
197
+ return `sha256:${(0, node_crypto_1.createHash)("sha256").update(value, "utf8").digest("hex")}`;
198
+ }
199
+ function hostProposalCommitment(proposal) {
200
+ return (0, sanctuary_authority_codec_1.authorityArtifactDigest)(PROPOSAL_DOMAIN, proposal);
201
+ }
202
+ function renderProposal(proposal, proposalDigest) {
203
+ const command = proposal.command.kind === "executable"
204
+ ? [
205
+ `<b>Executable:</b> <code>${html(proposal.command.executable)}</code>`,
206
+ `<b>Arguments:</b> <code>${html((0, sanctuary_authority_codec_1.canonicalAuthorityJson)(proposal.command.arguments))}</code>`,
207
+ ]
208
+ : [
209
+ `<b>Interpreter:</b> <code>${html(proposal.command.interpreter)}</code>`,
210
+ `<b>Arguments:</b> <code>${html((0, sanctuary_authority_codec_1.canonicalAuthorityJson)(proposal.command.arguments))}</code>`,
211
+ `<b>Complete script bytes:</b>\n<pre>${html(proposal.command.script)}</pre>`,
212
+ ];
213
+ return [
214
+ ROOT_HOST_PREFIX,
215
+ "",
216
+ "<b>Effect:</b> unrestricted root, potentially destructive and persistent",
217
+ "<b>Namespaces:</b> real Sanctuary host namespaces",
218
+ `<b>Target:</b> ${html(proposal.targetHost)} / ${html(proposal.targetResource)}`,
219
+ ...command,
220
+ `<b>Working directory profile:</b> ${html(proposal.workingDirectoryProfile)} (resolved path <code>/</code>)`,
221
+ `<b>Environment profile:</b> ${html(proposal.environmentProfile)}`,
222
+ `<b>Timeout:</b> ${proposal.timeoutMs} ms`,
223
+ `<b>Verification:</b> ${proposal.verification ? `${html(proposal.verification.profile)} / ${proposal.verification.expectedStateDigest}` : "none"}`,
224
+ `<b>Proposal commitment:</b> <code>${proposalDigest}</code>`,
225
+ "",
226
+ "Approved uid-0 code can read or change this gateway, its token and signing key; escape process controls; survive the receipt; install persistence; and invalidate future authority. Approval prevents an unapproved start. It cannot contain malicious code after approval.",
227
+ ].join("\n");
228
+ }
229
+ function initialState() {
230
+ return { schemaVersion: SCHEMA_VERSION, records: {}, callbackIds: {}, messageIds: {} };
231
+ }
232
+ function requireState(condition) {
233
+ if (!condition)
234
+ throw new Error("Sanctuary host authority state is malformed");
235
+ }
236
+ function validateStoredProposal(value, expectedHost) {
237
+ requireState(isObject(value));
238
+ const proposal = value;
239
+ requireState(exactKeys(proposal, [
240
+ "targetHost",
241
+ "targetResource",
242
+ "command",
243
+ "workingDirectoryProfile",
244
+ "environmentProfile",
245
+ "timeoutMs",
246
+ "verification",
247
+ "ownerObservation",
248
+ "expiresAt",
249
+ ]));
250
+ const { expiresAt, ...request } = proposal;
251
+ validateHostProposalRequest(request, expectedHost);
252
+ requireState(validTime(expiresAt));
253
+ }
254
+ function validateStoredArtifact(value, domain, options) {
255
+ return (0, sanctuary_authority_codec_1.verifyAuthorityPayload)({
256
+ artifact: value,
257
+ expectedDomain: domain,
258
+ expectedKeyId: options.keyId,
259
+ publicKey: options.publicKey,
260
+ });
261
+ }
262
+ function validateStoredRecord(registrationId, value, options) {
263
+ requireState(REGISTRATION_ID.test(registrationId));
264
+ requireState(isObject(value));
265
+ const record = value;
266
+ requireState(typeof record.state === "string");
267
+ const preparedKeys = [
268
+ "schemaVersion",
269
+ "state",
270
+ "registrationId",
271
+ "proposal",
272
+ "proposalDigest",
273
+ "prompt",
274
+ "promptDigest",
275
+ "approveHandle",
276
+ "denyHandle",
277
+ "approveHandleDigest",
278
+ "denyHandleDigest",
279
+ "preparedAt",
280
+ "expiresAt",
281
+ "nonce",
282
+ ];
283
+ const committedKeys = [
284
+ ...preparedKeys,
285
+ "telegramMessageId",
286
+ "registration",
287
+ "callbackQueryId",
288
+ "callbackObservationDigest",
289
+ "decidedAt",
290
+ "decision",
291
+ "permit",
292
+ "receipt",
293
+ "cardRevision",
294
+ "cardRenderedRevision",
295
+ ];
296
+ if (record.state === "orphaned") {
297
+ requireState(exactKeys(record, [...preparedKeys.filter((key) => key !== "approveHandle" && key !== "denyHandle"), "orphanedAt"]));
298
+ }
299
+ else if (record.state === "prepared") {
300
+ requireState(exactKeys(record, preparedKeys));
301
+ }
302
+ else {
303
+ requireState(["committed", "approved", "denied", "expired", "permitted", "executed", "retired"].includes(record.state));
304
+ requireState(exactKeys(record, committedKeys));
305
+ }
306
+ requireState(record.schemaVersion === SCHEMA_VERSION);
307
+ requireState(record.registrationId === registrationId);
308
+ validateStoredProposal(record.proposal, options.targetHost);
309
+ const proposal = record.proposal;
310
+ requireState(record.proposalDigest === hostProposalCommitment(proposal));
311
+ requireState(record.prompt === renderProposal(proposal, record.proposalDigest));
312
+ requireState(record.promptDigest === digestText(record.prompt));
313
+ requireState(validTime(record.preparedAt));
314
+ requireState(record.expiresAt === proposal.expiresAt);
315
+ requireState(Date.parse(record.expiresAt) === Date.parse(record.preparedAt) + APPROVAL_TTL_MS);
316
+ requireState(typeof record.nonce === "string");
317
+ requireState(/^[A-Za-z0-9_-]{43}$/u.test(record.nonce));
318
+ requireState(typeof record.approveHandleDigest === "string");
319
+ requireState(typeof record.denyHandleDigest === "string");
320
+ if (record.state === "orphaned") {
321
+ requireState(validTime(record.orphanedAt));
322
+ return;
323
+ }
324
+ requireState(printable(record.approveHandle, 256));
325
+ requireState(printable(record.denyHandle, 256));
326
+ requireState(record.approveHandleDigest === digestText(record.approveHandle));
327
+ requireState(record.denyHandleDigest === digestText(record.denyHandle));
328
+ if (record.state === "prepared")
329
+ return;
330
+ requireState(Number.isSafeInteger(record.telegramMessageId));
331
+ requireState(record.telegramMessageId > 0);
332
+ const registration = validateStoredArtifact(record.registration, REGISTRATION_DOMAIN, options);
333
+ requireState(registration.registrationId === registrationId);
334
+ requireState(registration.proposalDigest === record.proposalDigest);
335
+ requireState(registration.telegramMessageId === record.telegramMessageId);
336
+ requireState(registration.targetHost === options.targetHost);
337
+ requireState(registration.publicKeyDigest === options.publicKeyDigest);
338
+ const validCardRevisions = Object.keys(CARD_STATUS);
339
+ requireState(validCardRevisions.includes(record.cardRevision));
340
+ requireState(validCardRevisions.includes(record.cardRenderedRevision));
341
+ if (record.state === "committed") {
342
+ requireState(record.callbackQueryId === null);
343
+ requireState(record.callbackObservationDigest === null);
344
+ requireState(record.decidedAt === null);
345
+ requireState(record.decision === null);
346
+ requireState(record.permit === null);
347
+ requireState(record.receipt === null);
348
+ requireState(record.cardRevision === record.state);
349
+ return;
350
+ }
351
+ if (record.state === "expired" && record.callbackQueryId === null) {
352
+ requireState(record.callbackObservationDigest === null);
353
+ requireState(record.decidedAt === null);
354
+ requireState(record.decision === null);
355
+ requireState(record.permit === null);
356
+ requireState(record.receipt === null);
357
+ requireState(record.cardRevision === "expired");
358
+ return;
359
+ }
360
+ requireState(printable(record.callbackQueryId, 256));
361
+ requireState(typeof record.callbackObservationDigest === "string");
362
+ requireState(DIGEST.test(record.callbackObservationDigest));
363
+ requireState(validTime(record.decidedAt));
364
+ const decision = validateStoredArtifact(record.decision, DECISION_DOMAIN, options);
365
+ requireState(decision.registrationId === registrationId);
366
+ requireState(decision.callbackQueryId === record.callbackQueryId);
367
+ requireState(decision.callbackObservationDigest === record.callbackObservationDigest);
368
+ requireState(decision.decidedAt === record.decidedAt);
369
+ requireState(decision.decision === (record.state === "denied" ? "deny" : "approve"));
370
+ if (record.state === "approved" || record.state === "denied" || record.state === "expired") {
371
+ requireState(record.permit === null);
372
+ requireState(record.receipt === null);
373
+ requireState(record.cardRevision === (record.state === "expired" ? "expired" : record.state));
374
+ return;
375
+ }
376
+ const permit = validateStoredArtifact(record.permit, PERMIT_DOMAIN, options);
377
+ requireState(permit.registrationId === registrationId);
378
+ requireState(permit.targetHost === options.targetHost);
379
+ requireState(permit.publicKeyDigest === options.publicKeyDigest);
380
+ if (record.state === "permitted") {
381
+ requireState(record.receipt === null);
382
+ requireState(record.cardRevision === "approved");
383
+ return;
384
+ }
385
+ const receipt = validateStoredArtifact(record.receipt, record.state === "retired" ? RETIREMENT_DOMAIN : RECEIPT_DOMAIN, options);
386
+ requireState(receipt.registrationId === registrationId);
387
+ requireState(receipt.permitId === permit.permitId);
388
+ requireState(receipt.permitDigest === (0, sanctuary_authority_codec_1.authorityArtifactDigest)(PERMIT_DOMAIN, permit));
389
+ requireState(receipt.targetHost === options.targetHost);
390
+ requireState(receipt.publicKeyDigest === options.publicKeyDigest);
391
+ requireState(record.cardRevision === (record.state === "retired" ? "retired" : `executed_${String(receipt.state)}`));
392
+ }
393
+ function validateHostAuthorityState(value, options) {
394
+ requireState(isObject(value));
395
+ const state = value;
396
+ requireState(exactKeys(state, ["schemaVersion", "records", "callbackIds", "messageIds"]));
397
+ requireState(state.schemaVersion === SCHEMA_VERSION);
398
+ requireState(isObject(state.records));
399
+ requireState(isObject(state.callbackIds));
400
+ requireState(isObject(state.messageIds));
401
+ const records = state.records;
402
+ for (const [registrationId, record] of Object.entries(records)) {
403
+ validateStoredRecord(registrationId, record, options);
404
+ }
405
+ const expectedCallbackIds = {};
406
+ const expectedMessageIds = {};
407
+ for (const [registrationId, record] of Object.entries(records)) {
408
+ if ("telegramMessageId" in record)
409
+ expectedMessageIds[String(record.telegramMessageId)] = registrationId;
410
+ if ("callbackQueryId" in record && record.callbackQueryId)
411
+ expectedCallbackIds[record.callbackQueryId] = registrationId;
412
+ }
413
+ requireState((0, sanctuary_authority_codec_1.canonicalAuthorityJson)(state.callbackIds) === (0, sanctuary_authority_codec_1.canonicalAuthorityJson)(expectedCallbackIds));
414
+ requireState((0, sanctuary_authority_codec_1.canonicalAuthorityJson)(state.messageIds) === (0, sanctuary_authority_codec_1.canonicalAuthorityJson)(expectedMessageIds));
415
+ return state;
416
+ }
417
+ function sanctuaryHostAuthorityStatePath(agentRoot) {
418
+ return path.join(agentRoot, "authority", "host-authority.json");
419
+ }
420
+ class FileSanctuaryHostAuthority {
421
+ #statePath;
422
+ #options;
423
+ constructor(agentRoot, options) {
424
+ if (!path.isAbsolute(agentRoot))
425
+ throw new Error("Sanctuary host authority root must be absolute");
426
+ this.#statePath = sanctuaryHostAuthorityStatePath(agentRoot);
427
+ this.#options = options;
428
+ }
429
+ prepare(request) {
430
+ const now = this.#now();
431
+ validateHostProposalRequest(request, this.#options.targetHost);
432
+ const proposal = {
433
+ ...request,
434
+ expiresAt: new Date(Date.parse(now) + APPROVAL_TTL_MS).toISOString(),
435
+ };
436
+ const observation = this.#options.resolveOwnerObservation({
437
+ updateId: proposal.ownerObservation.updateId,
438
+ observationDigest: proposal.ownerObservation.digest,
439
+ });
440
+ if (!observation
441
+ || (0, sanctuary_authority_codec_1.authorityArtifactDigest)(observation.domain, observation.payload) !== proposal.ownerObservation.digest
442
+ || observation.payload.targetHost !== this.#options.targetHost
443
+ || observation.payload.botId !== this.#options.botId
444
+ || observation.payload.publicKeyDigest !== this.#options.publicKeyDigest
445
+ || !observation.payload.ownerEligible
446
+ || observation.payload.userId !== this.#options.ownerUserId
447
+ || observation.payload.chatId !== this.#options.ownerChatId
448
+ || observation.payload.updateId !== proposal.ownerObservation.updateId
449
+ || observation.payload.userId !== proposal.ownerObservation.userId
450
+ || observation.payload.chatId !== proposal.ownerObservation.chatId
451
+ || observation.payload.messageId !== proposal.ownerObservation.messageId) {
452
+ throw new Error("Sanctuary host owner observation is not root-authorized");
453
+ }
454
+ const proposalDigest = hostProposalCommitment(proposal);
455
+ const registrationNonce = this.#nonce();
456
+ const registrationId = `hostreg-${registrationNonce}`;
457
+ const approveHandle = `ouh:a:${this.#nonce()}`;
458
+ const denyHandle = `ouh:d:${this.#nonce()}`;
459
+ const prompt = renderProposal(proposal, proposalDigest);
460
+ if (prompt.length > 4_096)
461
+ throw new Error("Sanctuary host approval prompt is too large");
462
+ const record = {
463
+ schemaVersion: SCHEMA_VERSION,
464
+ state: "prepared",
465
+ registrationId,
466
+ proposal,
467
+ proposalDigest,
468
+ prompt,
469
+ promptDigest: digestText(prompt),
470
+ approveHandle,
471
+ denyHandle,
472
+ approveHandleDigest: digestText(approveHandle),
473
+ denyHandleDigest: digestText(denyHandle),
474
+ preparedAt: now,
475
+ expiresAt: proposal.expiresAt,
476
+ nonce: registrationNonce,
477
+ };
478
+ this.#transaction((state) => {
479
+ if (Object.values(state.records).some((record) => record.state === "prepared" || record.state === "committed" || record.state === "approved" || record.state === "permitted")) {
480
+ throw new Error("Sanctuary host approval is already active");
481
+ }
482
+ if (state.records[registrationId])
483
+ throw new Error("Sanctuary host registration id already exists");
484
+ state.records[registrationId] = record;
485
+ });
486
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_host_approval_prepared", message: "Sanctuary host approval intent durably prepared", meta: { proposalDigest } });
487
+ return {
488
+ registrationId,
489
+ proposalDigest,
490
+ prompt,
491
+ expiresAt: proposal.expiresAt,
492
+ replyMarkup: {
493
+ inline_keyboard: [[
494
+ { text: "Approve root execution", callback_data: approveHandle },
495
+ { text: "Deny", callback_data: denyHandle },
496
+ ]],
497
+ },
498
+ };
499
+ }
500
+ commit(input) {
501
+ if (!REGISTRATION_ID.test(input.registrationId)
502
+ || !Number.isSafeInteger(input.telegramMessageId)
503
+ || input.telegramMessageId <= 0) {
504
+ throw new Error("Sanctuary host registration commit is invalid");
505
+ }
506
+ let registration;
507
+ this.#transaction((state) => {
508
+ const current = state.records[input.registrationId];
509
+ if (!current || current.state !== "prepared")
510
+ throw new Error("Sanctuary host registration is not prepared");
511
+ if (state.messageIds[String(input.telegramMessageId)])
512
+ throw new Error("Sanctuary host approval message already exists");
513
+ registration = (0, sanctuary_authority_codec_1.signAuthorityPayload)({
514
+ domain: REGISTRATION_DOMAIN,
515
+ keyId: this.#options.keyId,
516
+ privateKey: this.#options.privateKey,
517
+ payload: {
518
+ targetHost: this.#options.targetHost,
519
+ botId: this.#options.botId,
520
+ targetResource: current.proposal.targetResource,
521
+ registrationId: current.registrationId,
522
+ proposalDigest: current.proposalDigest,
523
+ ownerObservationDigest: current.proposal.ownerObservation.digest,
524
+ ownerUpdateId: current.proposal.ownerObservation.updateId,
525
+ ownerUserId: current.proposal.ownerObservation.userId,
526
+ ownerChatId: current.proposal.ownerObservation.chatId,
527
+ ownerMessageId: current.proposal.ownerObservation.messageId,
528
+ command: current.proposal.command,
529
+ workingDirectoryProfile: current.proposal.workingDirectoryProfile,
530
+ environmentProfile: current.proposal.environmentProfile,
531
+ timeoutMs: current.proposal.timeoutMs,
532
+ verification: current.proposal.verification,
533
+ prompt: current.prompt,
534
+ promptDigest: current.promptDigest,
535
+ displayedProposalDigest: current.proposalDigest,
536
+ marker: ROOT_HOST_PREFIX,
537
+ telegramMessageId: input.telegramMessageId,
538
+ approveHandleDigest: current.approveHandleDigest,
539
+ denyHandleDigest: current.denyHandleDigest,
540
+ registeredAt: this.#now(),
541
+ expiresAt: current.expiresAt,
542
+ nonce: current.nonce,
543
+ publicKeyDigest: this.#options.publicKeyDigest,
544
+ },
545
+ });
546
+ const committed = {
547
+ ...current,
548
+ state: "committed",
549
+ telegramMessageId: input.telegramMessageId,
550
+ registration,
551
+ callbackQueryId: null,
552
+ callbackObservationDigest: null,
553
+ decidedAt: null,
554
+ decision: null,
555
+ permit: null,
556
+ receipt: null,
557
+ cardRevision: "committed",
558
+ cardRenderedRevision: "committed",
559
+ };
560
+ state.records[current.registrationId] = committed;
561
+ state.messageIds[String(input.telegramMessageId)] = current.registrationId;
562
+ });
563
+ return registration;
564
+ }
565
+ decide(input) {
566
+ if (!CALLBACK_ID.test(input.callbackQueryId)
567
+ || !printable(input.callbackData, 256)
568
+ || !Number.isSafeInteger(input.telegramMessageId)
569
+ || input.telegramMessageId <= 0
570
+ || input.userId !== this.#options.ownerUserId
571
+ || input.chatId !== this.#options.ownerChatId
572
+ || !DIGEST.test(input.callbackObservationDigest)
573
+ || !validTime(input.decidedAt)) {
574
+ throw new Error("Sanctuary host decision is invalid");
575
+ }
576
+ let decision = null;
577
+ this.#transaction((state) => {
578
+ if (state.callbackIds[input.callbackQueryId])
579
+ throw new Error("Sanctuary host callback is already consumed");
580
+ const registrationId = state.messageIds[String(input.telegramMessageId)];
581
+ const current = registrationId ? state.records[registrationId] : null;
582
+ if (!current || !("approveHandle" in current))
583
+ throw new Error("Sanctuary host registration state changed");
584
+ const approved = input.callbackData === current.approveHandle;
585
+ const denied = input.callbackData === current.denyHandle;
586
+ if (!approved && !denied)
587
+ throw new Error("Sanctuary host callback handle is invalid");
588
+ if (current.state !== "committed")
589
+ return false;
590
+ if (Date.parse(input.decidedAt) > Date.parse(current.expiresAt))
591
+ throw new Error("Sanctuary host approval expired");
592
+ const outcome = approved ? "approve" : "deny";
593
+ decision = (0, sanctuary_authority_codec_1.signAuthorityPayload)({
594
+ domain: DECISION_DOMAIN,
595
+ keyId: this.#options.keyId,
596
+ privateKey: this.#options.privateKey,
597
+ payload: {
598
+ targetHost: this.#options.targetHost,
599
+ registrationId: current.registrationId,
600
+ registrationDigest: (0, sanctuary_authority_codec_1.authorityArtifactDigest)(current.registration.domain, current.registration.payload),
601
+ decision: outcome,
602
+ callbackQueryId: input.callbackQueryId,
603
+ callbackObservationDigest: input.callbackObservationDigest,
604
+ decidedAt: input.decidedAt,
605
+ nonce: this.#nonce(),
606
+ publicKeyDigest: this.#options.publicKeyDigest,
607
+ },
608
+ });
609
+ state.records[current.registrationId] = {
610
+ ...current,
611
+ state: approved ? "approved" : "denied",
612
+ callbackQueryId: input.callbackQueryId,
613
+ callbackObservationDigest: input.callbackObservationDigest,
614
+ decidedAt: input.decidedAt,
615
+ decision,
616
+ cardRevision: approved ? "approved" : "denied",
617
+ };
618
+ state.callbackIds[input.callbackQueryId] = current.registrationId;
619
+ });
620
+ return decision;
621
+ }
622
+ reconcilePrepared() {
623
+ const orphaned = [];
624
+ this.#transaction((state) => {
625
+ const now = this.#now();
626
+ for (const [registrationId, record] of Object.entries(state.records)) {
627
+ if (record.state !== "prepared")
628
+ continue;
629
+ const { approveHandle: _approveHandle, denyHandle: _denyHandle, ...safe } = record;
630
+ state.records[registrationId] = { ...safe, state: "orphaned", orphanedAt: now };
631
+ orphaned.push(registrationId);
632
+ }
633
+ });
634
+ return orphaned;
635
+ }
636
+ // Caller has fenced ingress. Journal refusal intent in the registration before
637
+ // reserving its replay tombstone, so interruption cannot turn it into execution.
638
+ retireRegistrations(ledger) {
639
+ this.reconcilePrepared();
640
+ const pending = [];
641
+ this.#transaction((state) => {
642
+ for (const [id, record] of Object.entries(state.records)) {
643
+ if (record.state === "committed" || record.state === "approved") {
644
+ state.records[id] = { ...record, state: "expired", cardRevision: "expired" };
645
+ }
646
+ else if (record.state === "permitted") {
647
+ const permit = record.permit;
648
+ if (ledger.read(String(permit.payload.permitId))) {
649
+ pending.push(id);
650
+ continue;
651
+ }
652
+ const receipt = (0, sanctuary_authority_codec_1.signAuthorityPayload)({
653
+ domain: RETIREMENT_DOMAIN, keyId: this.#options.keyId, privateKey: this.#options.privateKey,
654
+ payload: {
655
+ registrationId: id, permitId: permit.payload.permitId,
656
+ permitDigest: (0, sanctuary_authority_codec_1.authorityArtifactDigest)(permit.domain, permit.payload),
657
+ targetHost: this.#options.targetHost, publicKeyDigest: this.#options.publicKeyDigest,
658
+ state: "refused", reason: "epoch_retired_before_reservation", retiredAt: this.#now(),
659
+ },
660
+ });
661
+ state.records[id] = { ...record, state: "retired", cardRevision: "retired", receipt };
662
+ }
663
+ }
664
+ });
665
+ for (const record of Object.values(this.#read().records)) {
666
+ if (record.state !== "retired")
667
+ continue;
668
+ const permit = record.permit;
669
+ const permitId = String(permit.payload.permitId);
670
+ const permitDigest = (0, sanctuary_authority_codec_1.authorityArtifactDigest)(permit.domain, permit.payload);
671
+ const outcomeDigest = (0, sanctuary_authority_codec_1.authorityArtifactDigest)(record.receipt.domain, record.receipt.payload);
672
+ const retiredAt = String(record.receipt.payload.retiredAt);
673
+ const current = ledger.read(permitId);
674
+ if (current && (current.permitDigest !== permitDigest || (current.state !== "reserved" && (current.state !== "refused" || current.outcomeDigest !== outcomeDigest))))
675
+ throw new Error("Sanctuary retired permit ledger changed");
676
+ if (!current)
677
+ ledger.reserve({ permitId, nonce: String(permit.payload.nonce), permitDigest, reservedAt: retiredAt });
678
+ if (!current || current.state === "reserved")
679
+ ledger.terminalize({ permitId, state: "refused", outcomeDigest, updatedAt: retiredAt });
680
+ }
681
+ return pending;
682
+ }
683
+ issuePermit(input) {
684
+ if (!isObject(input)
685
+ || !exactKeys(input, [
686
+ "registrationId",
687
+ "residentFriendId",
688
+ "relationshipProfileId",
689
+ "relationshipProfileVersion",
690
+ "requestId",
691
+ "sessionKey",
692
+ "sessionEventId",
693
+ "residentApprovalId",
694
+ "stewardPolicy",
695
+ ])
696
+ || !REGISTRATION_ID.test(input.registrationId)
697
+ || !printable(input.residentFriendId, 512)
698
+ || !printable(input.relationshipProfileId, 256)
699
+ || !Number.isSafeInteger(input.relationshipProfileVersion)
700
+ || input.relationshipProfileVersion < 1
701
+ || !printable(input.requestId, 512)
702
+ || !printable(input.sessionKey, 1_024)
703
+ || !printable(input.sessionEventId, 512)
704
+ || !printable(input.residentApprovalId, 512)) {
705
+ throw new Error("Sanctuary host permit correlation is invalid");
706
+ }
707
+ if (input.stewardPolicy !== null && (!isObject(input.stewardPolicy)
708
+ || !exactKeys(input.stewardPolicy, ["key", "version", "digest"])
709
+ || !printable(input.stewardPolicy.key, 256)
710
+ || !Number.isSafeInteger(input.stewardPolicy.version)
711
+ || input.stewardPolicy.version < 1
712
+ || typeof input.stewardPolicy.digest !== "string"
713
+ || !DIGEST.test(input.stewardPolicy.digest))) {
714
+ throw new Error("Sanctuary host permit steward policy is invalid");
715
+ }
716
+ let permit;
717
+ let issueWindowExpired = false;
718
+ this.#transaction((state) => {
719
+ const current = state.records[input.registrationId];
720
+ if (!current || current.state !== "approved" || !current.decision || !current.decidedAt || !current.callbackObservationDigest) {
721
+ throw new Error("Sanctuary host approval state cannot issue a permit");
722
+ }
723
+ const issuedAt = this.#now();
724
+ if (Date.parse(issuedAt) < Date.parse(current.decidedAt)) {
725
+ throw new Error("Sanctuary host permit issue window expired");
726
+ }
727
+ if (Date.parse(issuedAt) > Date.parse(current.decidedAt) + 120_000) {
728
+ state.records[current.registrationId] = { ...current, state: "expired", cardRevision: "expired" };
729
+ issueWindowExpired = true;
730
+ return;
731
+ }
732
+ const permitSeed = this.#nonce();
733
+ const permitId = `permit-${permitSeed}`;
734
+ const permitNonce = (0, node_crypto_1.createHash)("sha256").update(`permit-nonce\0${permitSeed}`, "utf8").digest("hex");
735
+ permit = (0, sanctuary_authority_codec_1.signAuthorityPayload)({
736
+ domain: PERMIT_DOMAIN,
737
+ keyId: this.#options.keyId,
738
+ privateKey: this.#options.privateKey,
739
+ payload: {
740
+ targetHost: this.#options.targetHost,
741
+ permitId,
742
+ registrationId: current.registrationId,
743
+ registrationDigest: (0, sanctuary_authority_codec_1.authorityArtifactDigest)(current.registration.domain, current.registration.payload),
744
+ ownerUserId: this.#options.ownerUserId,
745
+ ownerChatId: this.#options.ownerChatId,
746
+ ownerObservationDigest: current.proposal.ownerObservation.digest,
747
+ callbackObservationDigest: current.callbackObservationDigest,
748
+ residentFriendId: input.residentFriendId,
749
+ relationshipProfileId: input.relationshipProfileId,
750
+ relationshipProfileVersion: input.relationshipProfileVersion,
751
+ requestId: input.requestId,
752
+ sessionKey: input.sessionKey,
753
+ sessionEventId: input.sessionEventId,
754
+ residentApprovalId: input.residentApprovalId,
755
+ effectClass: "owner_approved_arbitrary_host",
756
+ executionProfile: "host.owner_approved.v1",
757
+ targetResource: current.proposal.targetResource,
758
+ command: current.proposal.command,
759
+ scriptDigest: current.proposal.command.kind === "script" ? digestText(current.proposal.command.script) : null,
760
+ workingDirectoryProfile: current.proposal.workingDirectoryProfile,
761
+ environmentProfile: current.proposal.environmentProfile,
762
+ environmentProfileDigest: digestText(current.proposal.environmentProfile),
763
+ timeoutMs: current.proposal.timeoutMs,
764
+ // Resident-supplied audit correlation, like the session IDs above; never a root authorization input.
765
+ stewardPolicy: input.stewardPolicy,
766
+ verification: current.proposal.verification,
767
+ issuedAt,
768
+ expiresAt: new Date(Date.parse(issuedAt) + 120_000).toISOString(),
769
+ nonce: permitNonce,
770
+ publicKeyDigest: this.#options.publicKeyDigest,
771
+ },
772
+ });
773
+ state.records[current.registrationId] = { ...current, state: "permitted", permit };
774
+ });
775
+ if (issueWindowExpired)
776
+ throw new Error("Sanctuary host permit issue window expired");
777
+ return permit;
778
+ }
779
+ completeExecution(registrationId, receipt) {
780
+ if (!REGISTRATION_ID.test(registrationId)) {
781
+ throw new Error("Sanctuary host execution receipt is invalid");
782
+ }
783
+ const receiptPayload = (0, sanctuary_authority_codec_1.verifyAuthorityPayload)({
784
+ artifact: receipt,
785
+ expectedDomain: RECEIPT_DOMAIN,
786
+ expectedKeyId: this.#options.keyId,
787
+ publicKey: this.#options.publicKey,
788
+ });
789
+ const receiptState = receiptPayload.state;
790
+ if (!["verified", "failed", "ambiguous"].includes(String(receiptState))) {
791
+ throw new Error("Sanctuary host execution receipt state is invalid");
792
+ }
793
+ this.#transaction((state) => {
794
+ const current = state.records[registrationId];
795
+ if (!current || current.state !== "permitted" || !current.permit) {
796
+ throw new Error("Sanctuary host execution state changed");
797
+ }
798
+ if (receiptPayload.targetHost !== this.#options.targetHost
799
+ || receiptPayload.registrationId !== registrationId
800
+ || receiptPayload.permitId !== current.permit.payload.permitId
801
+ || receiptPayload.permitDigest !== (0, sanctuary_authority_codec_1.authorityArtifactDigest)(current.permit.domain, current.permit.payload)
802
+ || receiptPayload.publicKeyDigest !== this.#options.publicKeyDigest
803
+ || !validTime(receiptPayload.startedAt)
804
+ || !validTime(receiptPayload.completedAt)
805
+ || Date.parse(receiptPayload.completedAt) < Date.parse(receiptPayload.startedAt)) {
806
+ throw new Error("Sanctuary host execution receipt permit changed");
807
+ }
808
+ state.records[registrationId] = {
809
+ ...current,
810
+ state: "executed",
811
+ receipt,
812
+ cardRevision: `executed_${receiptState}`,
813
+ };
814
+ });
815
+ }
816
+ expireRegistrations() {
817
+ const now = Date.parse(this.#now());
818
+ const expired = [];
819
+ this.#transaction((state) => {
820
+ for (const [registrationId, record] of Object.entries(state.records)) {
821
+ const committedExpired = record.state === "committed" && Date.parse(record.expiresAt) < now;
822
+ const approvedExpired = record.state === "approved"
823
+ && record.decidedAt !== null
824
+ && Date.parse(record.decidedAt) + 120_000 < now;
825
+ if (!committedExpired && !approvedExpired)
826
+ continue;
827
+ state.records[registrationId] = {
828
+ ...record,
829
+ state: "expired",
830
+ cardRevision: "expired",
831
+ };
832
+ expired.push(registrationId);
833
+ }
834
+ return expired.length > 0;
835
+ });
836
+ return expired;
837
+ }
838
+ pendingCardEdit(registrationId) {
839
+ if (!REGISTRATION_ID.test(registrationId))
840
+ throw new Error("Sanctuary host registration id is invalid");
841
+ const record = this.#read().records[registrationId];
842
+ if (!record || !("telegramMessageId" in record) || record.cardRevision === record.cardRenderedRevision)
843
+ return null;
844
+ return {
845
+ telegramMessageId: record.telegramMessageId,
846
+ text: `${ROOT_HOST_PREFIX}\n\n<b>Status:</b> ${CARD_STATUS[record.cardRevision]}\n<b>Proposal commitment:</b> <code>${record.proposalDigest}</code>`,
847
+ revision: record.cardRevision,
848
+ };
849
+ }
850
+ markCardEdited(registrationId, revision) {
851
+ this.#transaction((state) => {
852
+ const record = state.records[registrationId];
853
+ if (!record || !("cardRevision" in record) || record.cardRevision !== revision) {
854
+ throw new Error("Sanctuary host approval card revision changed");
855
+ }
856
+ state.records[registrationId] = { ...record, cardRenderedRevision: revision };
857
+ });
858
+ }
859
+ ownsMessage(telegramMessageId) {
860
+ if (!Number.isSafeInteger(telegramMessageId) || telegramMessageId <= 0)
861
+ return false;
862
+ return this.#read().messageIds[String(telegramMessageId)] !== undefined;
863
+ }
864
+ ownsPrompt(text) {
865
+ return typeof text === "string"
866
+ && (text.startsWith(ROOT_HOST_PREFIX) || text.startsWith("OURO ROOT HOST APPROVAL"));
867
+ }
868
+ ownsHandle(callbackData) {
869
+ if (typeof callbackData !== "string")
870
+ return false;
871
+ return Object.values(this.#read().records).some((record) => "approveHandle" in record && (record.approveHandle === callbackData || record.denyHandle === callbackData));
872
+ }
873
+ ownerMutationFrozen() {
874
+ return Object.values(this.#read().records).some((record) => record.state === "prepared");
875
+ }
876
+ decisionForCallback(callbackQueryId) {
877
+ if (!CALLBACK_ID.test(callbackQueryId))
878
+ return null;
879
+ const state = this.#read();
880
+ const registrationId = state.callbackIds[callbackQueryId];
881
+ if (!registrationId)
882
+ return null;
883
+ return state.records[registrationId].decision;
884
+ }
885
+ claimCallback(input) {
886
+ const callbackDigest = digestText(input.callbackData);
887
+ const record = Object.values(this.#read().records).find((record) => record.approveHandleDigest === callbackDigest || record.denyHandleDigest === callbackDigest
888
+ || ("telegramMessageId" in record && record.telegramMessageId === input.telegramMessageId && input.chatId === this.#options.ownerChatId));
889
+ if (!record)
890
+ return null;
891
+ let decision = null;
892
+ const previous = this.decisionForCallback(input.callbackQueryId);
893
+ if (previous) {
894
+ if (previous.payload.callbackObservationDigest === input.callbackObservationDigest)
895
+ decision = previous;
896
+ }
897
+ else if (record.state === "committed"
898
+ && record.telegramMessageId === input.telegramMessageId
899
+ && (record.approveHandle === input.callbackData || record.denyHandle === input.callbackData)
900
+ && input.userId === this.#options.ownerUserId
901
+ && input.chatId === this.#options.ownerChatId
902
+ && Date.parse(input.decidedAt) <= Date.parse(record.expiresAt)) {
903
+ decision = this.decide(input);
904
+ }
905
+ return (0, sanctuary_authority_codec_1.signAuthorityPayload)({
906
+ domain: "ouro.sanctuary.host-callback.v1",
907
+ keyId: this.#options.keyId,
908
+ privateKey: this.#options.privateKey,
909
+ payload: {
910
+ ...this.#identity(),
911
+ handled: true,
912
+ registrationId: record.registrationId,
913
+ callbackQueryId: input.callbackQueryId,
914
+ observationDigest: input.callbackObservationDigest,
915
+ decision,
916
+ },
917
+ });
918
+ }
919
+ attestStatus(status) {
920
+ return (0, sanctuary_authority_codec_1.signAuthorityPayload)({
921
+ domain: `ouro.sanctuary.host-${typeof status === "boolean" ? "health" : "status"}.v1`,
922
+ keyId: this.#options.keyId,
923
+ privateKey: this.#options.privateKey,
924
+ payload: {
925
+ ...this.#identity(),
926
+ observedAt: this.#now(),
927
+ ...(typeof status === "boolean" ? { healthy: status } : { status }),
928
+ },
929
+ });
930
+ }
931
+ #identity() {
932
+ return {
933
+ targetHost: this.#options.targetHost,
934
+ botId: this.#options.botId,
935
+ ownerUserId: this.#options.ownerUserId,
936
+ ownerChatId: this.#options.ownerChatId,
937
+ publicKeyDigest: this.#options.publicKeyDigest,
938
+ };
939
+ }
940
+ status(registrationId) {
941
+ if (!REGISTRATION_ID.test(registrationId))
942
+ throw new Error("Sanctuary host registration id is invalid");
943
+ const record = this.#read().records[registrationId];
944
+ if (!record)
945
+ return null;
946
+ return {
947
+ registrationId: record.registrationId,
948
+ state: record.state,
949
+ proposalDigest: record.proposalDigest,
950
+ expiresAt: record.expiresAt,
951
+ ...("telegramMessageId" in record ? { telegramMessageId: record.telegramMessageId } : {}),
952
+ ...("decision" in record && record.decision ? { decision: record.decision } : {}),
953
+ ...("permit" in record && record.permit ? {
954
+ permit: (0, sanctuary_authority_codec_1.signAuthorityPayload)({
955
+ domain: "ouro.sanctuary.host-attempt.v1",
956
+ keyId: this.#options.keyId,
957
+ privateKey: this.#options.privateKey,
958
+ payload: { ...record.permit.payload, permitDigest: (0, sanctuary_authority_codec_1.authorityArtifactDigest)(record.permit.domain, record.permit.payload) },
959
+ }),
960
+ } : {}),
961
+ ...("receipt" in record && record.receipt ? { receipt: record.receipt } : {}),
962
+ ...("cardRevision" in record ? { cardPending: record.cardRevision !== record.cardRenderedRevision } : {}),
963
+ };
964
+ }
965
+ #now() {
966
+ const value = this.#options.now?.() ?? new Date().toISOString();
967
+ if (!validTime(value))
968
+ throw new Error("Sanctuary host authority time is invalid");
969
+ return value;
970
+ }
971
+ #nonce() {
972
+ const value = this.#options.nonce?.() ?? (0, node_crypto_1.randomBytes)(32).toString("base64url");
973
+ if (!/^[A-Za-z0-9_-]{43}$/u.test(value))
974
+ throw new Error("Sanctuary host authority nonce is invalid");
975
+ return value;
976
+ }
977
+ #read() {
978
+ return (0, session_transaction_1.withImmediateSessionTurnLease)(this.#statePath, (lease) => {
979
+ const transaction = (0, session_transaction_1.readSessionTransaction)(this.#statePath, lease);
980
+ if (transaction.value === null) {
981
+ if (transaction.bytes)
982
+ throw new Error("Sanctuary host authority state is malformed");
983
+ return initialState();
984
+ }
985
+ return validateHostAuthorityState(transaction.value, this.#options);
986
+ });
987
+ }
988
+ #transaction(mutate) {
989
+ (0, session_transaction_1.withImmediateSessionTurnLease)(this.#statePath, (lease) => {
990
+ const transaction = (0, session_transaction_1.readSessionTransaction)(this.#statePath, lease);
991
+ let state;
992
+ if (transaction.value === null) {
993
+ if (transaction.bytes)
994
+ throw new Error("Sanctuary host authority state is malformed");
995
+ state = initialState();
996
+ }
997
+ else {
998
+ state = validateHostAuthorityState(transaction.value, this.#options);
999
+ }
1000
+ if (mutate(state) === false)
1001
+ return;
1002
+ this.#options.beforeWrite?.();
1003
+ fs.chmodSync(path.dirname(this.#statePath), 0o700);
1004
+ (0, session_transaction_1.writeSessionTransaction)(this.#statePath, state, { lease, expectedRevision: transaction.revision });
1005
+ });
1006
+ }
1007
+ }
1008
+ exports.FileSanctuaryHostAuthority = FileSanctuaryHostAuthority;