@ouro.bot/cli 0.1.0-alpha.816 → 0.1.0-alpha.818

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/assets/sanctuary-host-launcher.sh +16 -0
  2. package/changelog.json +16 -0
  3. package/deploy/unraid/Dockerfile +6 -0
  4. package/deploy/unraid/README.txt +296 -62
  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/core.js +2 -1
  16. package/dist/heart/daemon/container-spec-auditor-main.js +8 -7
  17. package/dist/heart/daemon/container-spec-auditor.js +16 -9
  18. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +95 -91
  19. package/dist/heart/daemon/sanctuary-acceptance-harness.js +95 -169
  20. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +4 -5
  21. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  22. package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
  23. package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
  24. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  25. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +780 -0
  26. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  27. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  28. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
  29. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  30. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  31. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  32. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  33. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
  34. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  35. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +615 -0
  36. package/dist/heart/daemon/sense-manager.js +20 -10
  37. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  38. package/dist/repertoire/tools.js +15 -6
  39. package/dist/senses/private-runtime.js +2 -2
  40. package/dist/senses/root-host-approval-port.js +345 -0
  41. package/dist/senses/root-host-approval-runtime.js +393 -0
  42. package/dist/senses/sanctuary-authority-resident.js +102 -0
  43. package/dist/senses/telegram-admission.js +7 -0
  44. package/dist/senses/telegram-attachments.js +5 -1
  45. package/dist/senses/telegram-authority-transport.js +231 -0
  46. package/dist/senses/telegram-client.js +31 -8
  47. package/dist/senses/telegram-entry.js +1 -1
  48. package/dist/senses/telegram.js +168 -28
  49. package/npm-shrinkwrap.json +2 -2
  50. package/package.json +1 -1
@@ -0,0 +1,345 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRootHostApprovalPort = createRootHostApprovalPort;
4
+ const runtime_1 = require("../nerves/runtime");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const sanctuary_authority_codec_1 = require("../heart/daemon/sanctuary-authority-codec");
7
+ const sanctuary_host_authority_1 = require("../heart/daemon/sanctuary-host-authority");
8
+ const IDENTITY_KEYS = ["targetHost", "botId", "ownerUserId", "ownerChatId", "publicKeyDigest"];
9
+ const REGISTRATION_KEYS = [
10
+ ...IDENTITY_KEYS, "targetResource", "registrationId", "proposalDigest", "ownerObservationDigest", "ownerUpdateId", "ownerMessageId",
11
+ "command", "workingDirectoryProfile", "environmentProfile", "timeoutMs", "verification", "prompt", "promptDigest", "displayedProposalDigest",
12
+ "marker", "telegramMessageId", "approveHandleDigest", "denyHandleDigest", "registeredAt", "expiresAt", "nonce",
13
+ ];
14
+ const DECISION_KEYS = ["targetHost", "publicKeyDigest", "registrationId", "registrationDigest", "decision", "callbackQueryId", "callbackObservationDigest", "decidedAt", "nonce"];
15
+ const CORRELATION_KEYS = ["registrationId", "residentFriendId", "relationshipProfileId", "relationshipProfileVersion", "requestId", "sessionKey", "sessionEventId", "residentApprovalId", "stewardPolicy"];
16
+ const PERMIT_KEYS = [
17
+ "targetHost", "publicKeyDigest", "permitId", "registrationId", "registrationDigest", "ownerUserId", "ownerChatId", "ownerObservationDigest", "callbackObservationDigest",
18
+ ...CORRELATION_KEYS.slice(1), "effectClass", "executionProfile", "targetResource", "command", "scriptDigest", "workingDirectoryProfile",
19
+ "environmentProfile", "environmentProfileDigest", "timeoutMs", "verification", "issuedAt", "expiresAt", "nonce", "permitDigest",
20
+ ];
21
+ const RECEIPT_KEYS = [
22
+ "targetHost", "publicKeyDigest", "permitId", "permitDigest", "registrationId", "state", "startedAt", "completedAt", "exitCode", "signal", "timedOut", "cancelled",
23
+ "outputOverflow", "stdoutDigest", "stderrDigest", "stdoutBytes", "stderrBytes", "stdoutExcerpt", "stderrExcerpt", "cleanup", "containment", "verificationBefore", "verificationAfter", "errorCategory",
24
+ ];
25
+ const DIGEST = /^sha256:[a-f0-9]{64}$/u;
26
+ const NONCE = /^[A-Za-z0-9_-]{43}$/u;
27
+ const REGISTRATION_ID = /^hostreg-[A-Za-z0-9_-]{43}$/u;
28
+ const PERMIT_ID = /^permit-[A-Za-z0-9_-]{43}$/u;
29
+ const HEALTH_TTL = 30_000;
30
+ const OBSERVATION_TTL = 300_000;
31
+ function requireValid(condition) {
32
+ if (!condition)
33
+ throw new Error("Sanctuary root host authority response or request is invalid");
34
+ }
35
+ function object(value) {
36
+ requireValid(typeof value === "object" && value !== null && !Array.isArray(value));
37
+ }
38
+ function shape(value, required, optional = []) {
39
+ object(value);
40
+ requireValid(required.every((key) => Object.hasOwn(value, key)));
41
+ requireValid(Object.keys(value).every((key) => required.includes(key) || optional.includes(key)));
42
+ }
43
+ function text(value, maximum) {
44
+ return typeof value === "string" && /^[\x20-\x7e]+$/u.test(value) && value.length <= maximum;
45
+ }
46
+ function matches(value, pattern) {
47
+ return typeof value === "string" && pattern.test(value);
48
+ }
49
+ function integer(value, minimum = 0) {
50
+ return Number.isSafeInteger(value) && value >= minimum;
51
+ }
52
+ function time(value) {
53
+ requireValid(typeof value === "string");
54
+ const instant = Date.parse(value);
55
+ requireValid(Number.isFinite(instant));
56
+ requireValid(new Date(instant).toISOString() === value);
57
+ return instant;
58
+ }
59
+ function fresh(value, ttl) {
60
+ const age = Date.now() - value;
61
+ return age >= 0 && age < ttl;
62
+ }
63
+ function same(left, right) {
64
+ requireValid((0, sanctuary_authority_codec_1.canonicalAuthorityJson)(left) === (0, sanctuary_authority_codec_1.canonicalAuthorityJson)(right));
65
+ }
66
+ function digest(text) {
67
+ return `sha256:${(0, node_crypto_1.createHash)("sha256").update(text, "utf8").digest("hex")}`;
68
+ }
69
+ function artifactDigest(artifact) {
70
+ return (0, sanctuary_authority_codec_1.authorityArtifactDigest)(artifact.domain, artifact.payload);
71
+ }
72
+ function freeze(value) {
73
+ if (typeof value === "object" && value !== null) {
74
+ for (const nested of Object.values(value))
75
+ freeze(nested);
76
+ Object.freeze(value);
77
+ }
78
+ return value;
79
+ }
80
+ function correlation(value) {
81
+ shape(value, CORRELATION_KEYS);
82
+ requireValid(matches(value.registrationId, REGISTRATION_ID));
83
+ for (const [key, maximum] of [["residentFriendId", 512], ["relationshipProfileId", 256], ["requestId", 512], ["sessionKey", 1_024], ["sessionEventId", 512], ["residentApprovalId", 512]]) {
84
+ requireValid(text(value[key], maximum));
85
+ }
86
+ requireValid(integer(value.relationshipProfileVersion, 1));
87
+ if (value.stewardPolicy !== null) {
88
+ shape(value.stewardPolicy, ["key", "version", "digest"]);
89
+ requireValid(text(value.stewardPolicy.key, 256));
90
+ requireValid(integer(value.stewardPolicy.version, 1));
91
+ requireValid(matches(value.stewardPolicy.digest, DIGEST));
92
+ }
93
+ }
94
+ // Only the transport supplies these closures; caller-created metadata never enters their registry.
95
+ function createRootHostApprovalPort(client, verification, observations) {
96
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_root_approval_port_created", message: "Pinned Sanctuary root approval port created" });
97
+ const pins = Object.freeze({ ...verification });
98
+ let healthyAt = Number.NEGATIVE_INFINITY;
99
+ function verify(value, domain, keys) {
100
+ const payload = (0, sanctuary_authority_codec_1.verifyAuthorityPayload)({
101
+ artifact: value, expectedDomain: `ouro.sanctuary.host-${domain}.v1`, expectedKeyId: pins.expectedKeyId, publicKey: pins.publicKey,
102
+ });
103
+ shape(payload, keys);
104
+ requireValid(payload.targetHost === pins.expectedTargetHost);
105
+ requireValid(payload.publicKeyDigest === pins.expectedPublicKeyDigest);
106
+ return value;
107
+ }
108
+ function identity(payload) {
109
+ requireValid(payload.botId === pins.expectedBotId);
110
+ requireValid(payload.ownerUserId === pins.expectedOwnerUserId);
111
+ requireValid(payload.ownerChatId === pins.expectedOwnerChatId);
112
+ }
113
+ function registration(value) {
114
+ shape(value, ["registration", "registrationId", "telegramMessageId", "expiresAt"]);
115
+ const signed = verify(value.registration, "registration", REGISTRATION_KEYS);
116
+ const p = signed.payload;
117
+ identity(p);
118
+ requireValid(matches(p.registrationId, REGISTRATION_ID));
119
+ requireValid(integer(p.telegramMessageId, 1));
120
+ requireValid(matches(p.nonce, NONCE));
121
+ for (const field of ["proposalDigest", "ownerObservationDigest", "promptDigest", "approveHandleDigest", "denyHandleDigest"])
122
+ requireValid(matches(p[field], DIGEST));
123
+ same(p.displayedProposalDigest, p.proposalDigest);
124
+ same(p.marker, "<b>OURO ROOT HOST APPROVAL</b>");
125
+ requireValid(typeof p.prompt === "string" && p.prompt.length <= 4_096);
126
+ same(digest(p.prompt), p.promptDigest);
127
+ requireValid(p.prompt.startsWith(String(p.marker)));
128
+ requireValid(p.prompt.includes(String(p.proposalDigest)));
129
+ requireValid(time(p.expiresAt) >= time(p.registeredAt));
130
+ requireValid(time(p.expiresAt) <= time(p.registeredAt) + OBSERVATION_TTL);
131
+ const proposal = {
132
+ targetHost: p.targetHost, targetResource: p.targetResource, command: p.command, workingDirectoryProfile: p.workingDirectoryProfile,
133
+ environmentProfile: p.environmentProfile, timeoutMs: p.timeoutMs, verification: p.verification,
134
+ ownerObservation: { digest: p.ownerObservationDigest, updateId: p.ownerUpdateId, userId: p.ownerUserId, chatId: p.ownerChatId, messageId: p.ownerMessageId },
135
+ };
136
+ (0, sanctuary_host_authority_1.validateHostProposalRequest)(proposal, pins.expectedTargetHost);
137
+ same((0, sanctuary_authority_codec_1.authorityArtifactDigest)("ouro.sanctuary.host-proposal.v1", { ...proposal, expiresAt: p.expiresAt }), p.proposalDigest);
138
+ same({ registrationId: p.registrationId, telegramMessageId: p.telegramMessageId, expiresAt: p.expiresAt }, { registrationId: value.registrationId, telegramMessageId: value.telegramMessageId, expiresAt: value.expiresAt });
139
+ return freeze(value);
140
+ }
141
+ function verifyDecision(value) {
142
+ const result = verify(value, "decision", DECISION_KEYS);
143
+ const p = result.payload;
144
+ requireValid(matches(p.registrationId, REGISTRATION_ID));
145
+ requireValid(matches(p.registrationDigest, DIGEST));
146
+ requireValid(p.decision === "approve" || p.decision === "deny");
147
+ requireValid(text(p.callbackQueryId, 256));
148
+ requireValid(matches(p.callbackObservationDigest, DIGEST));
149
+ requireValid(matches(p.nonce, NONCE));
150
+ time(p.decidedAt);
151
+ return result;
152
+ }
153
+ function decision(value, reg) {
154
+ const result = verifyDecision(value);
155
+ const p = result.payload;
156
+ same(p.registrationId, reg.registrationId);
157
+ same(p.registrationDigest, artifactDigest(reg.registration));
158
+ requireValid(time(p.decidedAt) >= time(reg.registration.payload.registeredAt));
159
+ requireValid(time(p.decidedAt) <= time(reg.expiresAt));
160
+ return result;
161
+ }
162
+ function permit(value, reg, approved) {
163
+ const result = verify(value, "attempt", PERMIT_KEYS);
164
+ const p = result.payload;
165
+ requireValid(matches(p.permitDigest, DIGEST));
166
+ correlation(Object.fromEntries(CORRELATION_KEYS.map((key) => [key, p[key]])));
167
+ const r = reg.registration.payload;
168
+ for (const key of ["registrationId", "targetResource", "ownerUserId", "ownerChatId", "ownerObservationDigest", "command", "workingDirectoryProfile", "environmentProfile", "timeoutMs", "verification"])
169
+ same(p[key], r[key]);
170
+ same(p.registrationDigest, artifactDigest(reg.registration));
171
+ same(approved.payload.decision, "approve");
172
+ same(p.callbackObservationDigest, approved.payload.callbackObservationDigest);
173
+ same(p.effectClass, "owner_approved_arbitrary_host");
174
+ same(p.executionProfile, "host.owner_approved.v1");
175
+ requireValid(matches(p.permitId, PERMIT_ID));
176
+ requireValid(matches(p.nonce, /^[a-f0-9]{64}$/u));
177
+ const command = r.command;
178
+ same(p.scriptDigest, command.kind === "script" ? digest(command.script) : null);
179
+ same(p.environmentProfileDigest, digest(r.environmentProfile));
180
+ requireValid(time(p.issuedAt) >= time(approved.payload.decidedAt));
181
+ requireValid(time(p.issuedAt) <= time(approved.payload.decidedAt) + 120_000);
182
+ requireValid(time(p.expiresAt) > time(p.issuedAt));
183
+ requireValid(time(p.expiresAt) <= time(p.issuedAt) + 120_000);
184
+ return result;
185
+ }
186
+ function receipt(value, reg, permitted) {
187
+ const result = verify(value, "receipt", RECEIPT_KEYS);
188
+ const p = result.payload;
189
+ same(p.registrationId, reg.registrationId);
190
+ same(p.permitId, permitted.payload.permitId);
191
+ same(p.permitDigest, permitted.payload.permitDigest);
192
+ requireValid(typeof p.state === "string" && ["verified", "failed", "ambiguous"].includes(p.state));
193
+ requireValid(time(p.startedAt) >= time(permitted.payload.issuedAt));
194
+ requireValid(time(p.completedAt) >= time(p.startedAt));
195
+ requireValid(p.exitCode === null || integer(p.exitCode));
196
+ requireValid(p.signal === null || text(p.signal, 128));
197
+ for (const key of ["timedOut", "cancelled", "outputOverflow"])
198
+ requireValid(typeof p[key] === "boolean");
199
+ for (const stream of ["stdout", "stderr"]) {
200
+ requireValid(matches(p[`${stream}Digest`], DIGEST));
201
+ requireValid(integer(p[`${stream}Bytes`]));
202
+ const excerpt = p[`${stream}Excerpt`];
203
+ requireValid(typeof excerpt === "string" && excerpt.length <= 4_096);
204
+ }
205
+ requireValid(typeof p.cleanup === "string" && ["cgroup_empty", "cleanup_unproven"].includes(p.cleanup));
206
+ requireValid(typeof p.containment === "string" && ["approved_root_may_escape", "unprovable_after_approved_root_migration"].includes(p.containment));
207
+ requireValid(p.errorCategory === null || text(p.errorCategory, 256));
208
+ if (p.verificationBefore !== null) {
209
+ shape(p.verificationBefore, ["digest"]);
210
+ requireValid(matches(p.verificationBefore.digest, DIGEST));
211
+ }
212
+ if (p.verificationAfter !== null) {
213
+ shape(p.verificationAfter, ["matches", "digest"]);
214
+ requireValid(p.verificationAfter.matches === null || typeof p.verificationAfter.matches === "boolean");
215
+ requireValid(matches(p.verificationAfter.digest, DIGEST));
216
+ }
217
+ return result;
218
+ }
219
+ const port = {
220
+ pins,
221
+ async refresh() {
222
+ healthyAt = Number.NEGATIVE_INFINITY;
223
+ try {
224
+ requireValid(!observations.stopped());
225
+ const response = await client.request("host.status", { registrationId: null });
226
+ shape(response, ["health"]);
227
+ const p = verify(response.health, "health", [...IDENTITY_KEYS, "observedAt", "healthy"]).payload;
228
+ identity(p);
229
+ requireValid(p.healthy === true);
230
+ const observedAt = time(p.observedAt);
231
+ requireValid(fresh(observedAt, HEALTH_TTL));
232
+ healthyAt = observedAt;
233
+ return port.isHealthy();
234
+ }
235
+ catch {
236
+ return false;
237
+ }
238
+ },
239
+ isHealthy() {
240
+ return !observations.stopped() && fresh(healthyAt, HEALTH_TTL);
241
+ },
242
+ acceptsObservation(metadata) {
243
+ return port.isHealthy()
244
+ && metadata === observations.current()
245
+ && metadata.ownerEligible
246
+ && metadata.updateClass === "message"
247
+ && fresh(time(metadata.observedAt), OBSERVATION_TTL);
248
+ },
249
+ async register(proposal) {
250
+ const metadata = observations.current();
251
+ requireValid(metadata && port.acceptsObservation(metadata));
252
+ (0, sanctuary_host_authority_1.validateHostProposalRequest)(proposal, pins.expectedTargetHost);
253
+ same(proposal.ownerObservation, { digest: metadata.observationDigest, updateId: metadata.updateId, userId: metadata.userId, chatId: metadata.chatId, messageId: metadata.messageId });
254
+ const result = registration(await client.request("host.approval", { proposal }));
255
+ same(result.registration.payload.proposalDigest, (0, sanctuary_authority_codec_1.authorityArtifactDigest)("ouro.sanctuary.host-proposal.v1", { ...proposal, expiresAt: result.expiresAt }));
256
+ requireValid(fresh(time(result.registration.payload.registeredAt), OBSERVATION_TTL));
257
+ requireValid(Date.now() < time(result.expiresAt));
258
+ return result;
259
+ },
260
+ async status(value) {
261
+ const reg = registration(value);
262
+ requireValid(!observations.stopped());
263
+ const response = await client.request("host.status", { registrationId: reg.registrationId });
264
+ object(response);
265
+ const envelope = verify(response.authority, "status", [...IDENTITY_KEYS, "observedAt", "status"]);
266
+ identity(envelope.payload);
267
+ requireValid(fresh(time(envelope.payload.observedAt), HEALTH_TTL));
268
+ const status = envelope.payload.status;
269
+ shape(status, ["registrationId", "state", "proposalDigest", "expiresAt", "telegramMessageId", "cardPending", "execution"], ["decision", "permit", "receipt", "executionError", "maintenanceError"]);
270
+ const { authority: _authority, ...legacy } = response;
271
+ same(status, legacy);
272
+ same(status.registrationId, reg.registrationId);
273
+ same(status.proposalDigest, reg.registration.payload.proposalDigest);
274
+ same(status.expiresAt, reg.expiresAt);
275
+ same(status.telegramMessageId, reg.telegramMessageId);
276
+ requireValid(typeof status.cardPending === "boolean");
277
+ requireValid(typeof status.state === "string" && ["committed", "approved", "denied", "expired", "permitted", "executed"].includes(status.state));
278
+ requireValid(typeof status.execution === "string" && ["running", "reconciliation_required", "terminal"].includes(status.execution));
279
+ for (const key of ["executionError", "maintenanceError"])
280
+ if (key in status)
281
+ requireValid(text(status[key], 512));
282
+ let decided;
283
+ let permitted;
284
+ let received;
285
+ if ("decision" in status)
286
+ decided = decision(status.decision, reg);
287
+ if (["approved", "denied", "permitted", "executed"].includes(String(status.state))) {
288
+ requireValid(decided);
289
+ same(decided.payload.decision, status.state === "denied" ? "deny" : "approve");
290
+ }
291
+ if (status.state === "committed")
292
+ requireValid(!decided);
293
+ if ("permit" in status) {
294
+ requireValid(decided);
295
+ requireValid(["permitted", "executed"].includes(String(status.state)));
296
+ permitted = permit(status.permit, reg, decided);
297
+ }
298
+ if (["permitted", "executed"].includes(String(status.state)))
299
+ requireValid(permitted);
300
+ if ("receipt" in status) {
301
+ requireValid(permitted);
302
+ same(status.state, "executed");
303
+ received = receipt(status.receipt, reg, permitted);
304
+ }
305
+ if (status.state === "executed")
306
+ requireValid(received);
307
+ if (status.execution !== "terminal")
308
+ requireValid(["permitted", "executed"].includes(String(status.state)));
309
+ return freeze({
310
+ ...status,
311
+ statusDigest: artifactDigest(envelope),
312
+ });
313
+ },
314
+ async execute(value) {
315
+ requireValid(port.isHealthy());
316
+ correlation(value);
317
+ const response = await client.request("host.execute", { correlation: value });
318
+ shape(response, ["registrationId", "permitId", "state"]);
319
+ same(response.registrationId, value.registrationId);
320
+ requireValid(matches(response.permitId, PERMIT_ID));
321
+ same(response.state, "executing");
322
+ },
323
+ callbackForUpdate(update) {
324
+ const observation = observations.lookup(update);
325
+ if (!observation || observation.hostCallback === undefined)
326
+ return { handled: false };
327
+ const p = verify(observation.hostCallback, "callback", [...IDENTITY_KEYS, "handled", "registrationId", "callbackQueryId", "observationDigest", "decision"]).payload;
328
+ identity(p);
329
+ same(p.handled, true);
330
+ same(observation.metadata.updateClass, "callback");
331
+ same(p.observationDigest, observation.metadata.observationDigest);
332
+ same(p.callbackQueryId, observation.metadata.callbackQueryId);
333
+ requireValid(matches(p.registrationId, REGISTRATION_ID));
334
+ if (p.decision !== null) {
335
+ const d = verifyDecision(p.decision).payload;
336
+ same(d.registrationId, p.registrationId);
337
+ same(d.callbackQueryId, p.callbackQueryId);
338
+ same(d.callbackObservationDigest, p.observationDigest);
339
+ requireValid(observation.metadata.ownerEligible);
340
+ }
341
+ return { handled: true, registrationId: p.registrationId };
342
+ },
343
+ };
344
+ return Object.freeze(port);
345
+ }