@hellolynk-ai/codex-plugin 0.0.0-stage → 0.2.99

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 (123) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/LICENSE +92 -0
  3. package/README.md +86 -2
  4. package/bin/hellolynk-client-setup.mjs +2748 -0
  5. package/bin/hellolynk-mcp-config-shape.mjs +23 -0
  6. package/hellolynk-client-pack-manifest.json +619 -0
  7. package/hellolynk-client-pack-pin.json +4 -0
  8. package/package.json +58 -4
  9. package/plugins/hellolynk-codex/.app.json +3 -0
  10. package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
  11. package/plugins/hellolynk-codex/.mcp.json +19 -0
  12. package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
  13. package/plugins/hellolynk-codex/package.json +11 -0
  14. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
  15. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
  16. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
  17. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
  18. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
  19. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
  20. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  21. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
  22. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
  23. package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  24. package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
  25. package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
  26. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
  27. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
  28. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
  29. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
  30. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
  31. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
  32. package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
  33. package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
  34. package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
  35. package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
  36. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
  37. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
  38. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
  39. package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
  40. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
  41. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
  42. package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
  43. package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
  44. package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
  45. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
  46. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
  47. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
  48. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
  49. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
  50. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
  51. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
  52. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
  53. package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
  54. package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
  55. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
  56. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
  57. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
  58. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
  59. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
  60. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
  61. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
  62. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
  63. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  64. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
  65. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
  66. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
  67. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
  68. package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
  69. package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
  70. package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +160 -0
  71. package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
  72. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
  73. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
  74. package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
  75. package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
  76. package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
  77. package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
  78. package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
  79. package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
  80. package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
  81. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  82. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
  83. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
  84. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
  85. package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
  86. package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
  87. package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
  88. package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
  89. package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
  90. package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
  91. package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
  92. package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
  93. package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
  94. package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
  95. package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
  96. package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
  97. package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
  98. package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
  99. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
  100. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
  101. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
  102. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
  103. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
  104. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
  105. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
  106. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
  107. package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
  108. package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
  109. package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
  110. package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
  111. package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
  112. package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
  113. package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
  114. package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
  115. package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
  116. package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
  117. package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
  118. package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
  119. package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
  120. package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
  121. package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
  122. package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
  123. package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
@@ -0,0 +1,1234 @@
1
+ import crypto from "node:crypto";
2
+
3
+ import {
4
+ CONTACT_REQUEST_E2EE_AUTHORIZATION_SCOPE,
5
+ CONTACT_REQUEST_E2EE_INTENT_SOURCE,
6
+ CONTACT_REQUEST_E2EE_MESSAGE_TYPE,
7
+ createContactRequestIntroCommitment,
8
+ createContactRequestRecordDigest,
9
+ digestContactRequestIntroEnvelope,
10
+ validateContactRequestIntro,
11
+ } from "./hellolynk-contact-request-e2ee.mjs";
12
+
13
+ export const HELLOLYNK_HUMAN_RELATIONSHIP_CONTROLLER_CONTRACT =
14
+ "hellolynk-human-relationship-controller-v2";
15
+
16
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,240}$/;
17
+ const HELLOLYNK_ID = /^hellolynk:(intl|cn):[a-z0-9][a-z0-9._-]{2,179}$/;
18
+ const TARGET_CAPACITY = 64;
19
+ const DECISION_TARGET_STATES = Object.freeze({
20
+ accept: "accepted",
21
+ reject: "rejected",
22
+ block: "blocked",
23
+ });
24
+ const DECISION_TERMINAL_STATES = new Set([
25
+ "accepted",
26
+ "rejected",
27
+ "cancelled",
28
+ "blocked",
29
+ "expired",
30
+ ]);
31
+
32
+ export class HelloLynkHumanRelationshipControllerError extends Error {
33
+ constructor(code, options = {}) {
34
+ super(code, options.cause ? { cause: options.cause } : undefined);
35
+ this.name = "HelloLynkHumanRelationshipControllerError";
36
+ this.code = code;
37
+ this.httpStatus = Number.isInteger(options.httpStatus) ? options.httpStatus : null;
38
+ }
39
+ }
40
+
41
+ export function createHelloLynkHumanRelationshipController(options = {}) {
42
+ const cloud = requireMethods(options.cloud, [
43
+ "searchDirectory", "reserveContactRequest", "relayContactRequest",
44
+ "listContactRequests", "decideContactRequest", "getContactRequestDecisionState",
45
+ ], "human_relationship_cloud_invalid");
46
+ const e2ee = requireMethods(
47
+ options.e2ee,
48
+ ["sealContactRequestIntro"],
49
+ "human_relationship_e2ee_invalid",
50
+ );
51
+ const localStore = requireMethods(options.localStore, [
52
+ "listForHuman", "listPendingAckReceipts", "projectServerState",
53
+ "beginDecision", "abortDecision", "resolveDecision", "recoverDecision",
54
+ "listUnresolvedDecisions",
55
+ "clearResolvedForHuman",
56
+ ], "human_relationship_local_store_invalid");
57
+ const contactAuthorizationStore = options.contactAuthorizationStore == null
58
+ ? null
59
+ : requireMethods(
60
+ options.contactAuthorizationStore,
61
+ ["revokeAutonomousMessagingByHelloLynkId"],
62
+ "human_relationship_contact_authorization_store_invalid",
63
+ );
64
+ if (typeof options.runtimeHeaders !== "function") {
65
+ throw failure("human_relationship_runtime_headers_required");
66
+ }
67
+ const owner = Object.freeze({
68
+ principalId: safeId(options.principalId, "human_relationship_owner_invalid"),
69
+ hellolynkId: safeHelloLynkId(options.hellolynkId, "human_relationship_owner_invalid"),
70
+ deviceId: safeId(options.deviceId, "human_relationship_owner_invalid"),
71
+ identityKeyId: safeId(options.identityKeyId, "human_relationship_owner_invalid"),
72
+ });
73
+ const now = typeof options.now === "function" ? options.now : () => Date.now();
74
+ const targets = new Map();
75
+ const sendAttempts = new Map();
76
+
77
+ async function search(input = {}) {
78
+ exactKeys(input, ["mode", "query"], "human_relationship_search_invalid");
79
+ const mode = String(input.mode || "");
80
+ if (!new Set(["hellolynk_id", "nickname"]).has(mode)) {
81
+ throw failure("human_relationship_search_mode_invalid");
82
+ }
83
+ const query = String(input.query || "");
84
+ if (!query || query.length > 200 || (mode === "hellolynk_id" && !HELLOLYNK_ID.test(query))) {
85
+ throw failure("human_relationship_search_query_invalid");
86
+ }
87
+ pruneTargets();
88
+ const result = await cloud.searchDirectory({
89
+ clientSearchId: `human_search_${crypto.randomBytes(18).toString("hex")}`,
90
+ mode,
91
+ query,
92
+ });
93
+ const publicResults = [];
94
+ for (const item of array(result?.results, "human_relationship_search_response_invalid")) {
95
+ const binding = record(item?.hostTarget, "human_relationship_search_response_invalid");
96
+ const expiresAtMs = Date.parse(String(binding.expiresAt || ""));
97
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now()) {
98
+ throw failure("human_relationship_search_response_invalid");
99
+ }
100
+ const targetRef = `human_target_${crypto.randomBytes(24).toString("hex")}`;
101
+ targets.set(targetRef, Object.freeze({
102
+ targetRef,
103
+ cloudTargetRef: nonEmpty(item.directoryTargetRef, "human_relationship_search_response_invalid"),
104
+ principalId: safeId(binding.principalId, "human_relationship_search_response_invalid"),
105
+ hellolynkId: safeHelloLynkId(binding.hellolynkId, "human_relationship_search_response_invalid"),
106
+ expiresAtMs,
107
+ }));
108
+ publicResults.push(Object.freeze({
109
+ targetRef,
110
+ displayName: safeDisplayName(item.displayName),
111
+ hellolynkId: mode === "hellolynk_id" ? safeHelloLynkId(item.hellolynkId, "human_relationship_search_response_invalid") : null,
112
+ expiresAt: new Date(expiresAtMs).toISOString(),
113
+ }));
114
+ }
115
+ trimTargets();
116
+ return Object.freeze({
117
+ results: Object.freeze(publicResults),
118
+ quota: publicDirectoryQuota(result?.quota),
119
+ exactMatchOnly: true,
120
+ modelCalls: 0,
121
+ });
122
+ }
123
+
124
+ async function send(input = {}) {
125
+ exactKeys(input, ["targetRef", "introText"], "human_relationship_send_invalid");
126
+ const targetRef = String(input.targetRef || "");
127
+ pruneTargets();
128
+ const target = targets.get(targetRef);
129
+ if (!target) throw failure("human_relationship_target_expired");
130
+ let attempt = sendAttempts.get(targetRef);
131
+ if (attempt?.result) return attempt.result;
132
+ const finish = (status, quota = null, serverStoredCiphertext = false) => {
133
+ attempt.phase = status;
134
+ attempt.result = publicSendResult(status, quota, serverStoredCiphertext);
135
+ return attempt.result;
136
+ };
137
+ const accepted = validateIntro(input.introText);
138
+ if (!attempt) {
139
+ const commitment = createContactRequestIntroCommitment(accepted.text);
140
+ attempt = {
141
+ phase: "prepared",
142
+ clientRequestId: `human_contact_${crypto.randomBytes(18).toString("hex")}`,
143
+ commitment,
144
+ requestDigest: createContactRequestRecordDigest({
145
+ recipientPrincipalId: target.principalId,
146
+ introDigest: commitment.introDigest,
147
+ introWeight: commitment.introWeight,
148
+ }),
149
+ };
150
+ sendAttempts.set(targetRef, attempt);
151
+ } else {
152
+ const repeated = createContactRequestIntroCommitment(accepted.text, {
153
+ introDigestSalt: attempt.commitment.introDigestSalt,
154
+ });
155
+ if (repeated.introDigest !== attempt.commitment.introDigest) {
156
+ throw failure("human_relationship_retry_input_conflict");
157
+ }
158
+ }
159
+
160
+ // Concurrent callers for this exact attempt share its result and envelope.
161
+ // Publish the promise before invoking cloud or crypto dependencies.
162
+ if (attempt.pending) return attempt.pending;
163
+ attempt.pending = Promise.resolve().then(sendPreparedAttempt).finally(() => {
164
+ attempt.pending = null;
165
+ });
166
+ return attempt.pending;
167
+
168
+ async function sendPreparedAttempt() {
169
+ const common = {
170
+ directoryTargetRef: target.cloudTargetRef,
171
+ clientRequestId: attempt.clientRequestId,
172
+ introDigest: attempt.commitment.introDigest,
173
+ introWeight: attempt.commitment.introWeight,
174
+ requestDigest: attempt.requestDigest,
175
+ };
176
+ let reserved;
177
+ try {
178
+ reserved = await cloud.reserveContactRequest({
179
+ ...common,
180
+ runtimeHeaders: options.runtimeHeaders,
181
+ });
182
+ } catch (error) {
183
+ if (error?.httpStatus === 409
184
+ && (error?.code === "contact_request_recipient_prekey_unavailable"
185
+ || error?.code === "contact_request_recipient_endpoint_ambiguous")) {
186
+ if (sendAttempts.get(targetRef) === attempt) sendAttempts.delete(targetRef);
187
+ throw error;
188
+ }
189
+ if (!Number.isInteger(error?.httpStatus) || error.httpStatus >= 500) {
190
+ return finish("delivery_unknown");
191
+ }
192
+ throw error;
193
+ }
194
+ const handoff = reserved?.handoff;
195
+ if (!handoff) {
196
+ return finish("delivery_unknown", reserved?.quota);
197
+ }
198
+ validateHandoff(handoff, owner, target, e2ee.identityKeyId || owner.identityKeyId);
199
+ const createdAtMs = now();
200
+ const createdAt = new Date(createdAtMs).toISOString();
201
+ const expiresAt = new Date(Math.min(
202
+ createdAtMs + 7 * 24 * 60 * 60 * 1000,
203
+ Date.parse(String(handoff.claimExpiresAt || "")),
204
+ )).toISOString();
205
+ if (!Number.isFinite(Date.parse(expiresAt)) || Date.parse(expiresAt) <= createdAtMs) {
206
+ throw failure("human_relationship_contact_handoff_expired");
207
+ }
208
+ const protectedMetadata = {
209
+ attemptId: safeId(reserved.attempt?.attemptId, "human_relationship_contact_handoff_invalid"),
210
+ clientRequestId: attempt.clientRequestId,
211
+ requestDigest: attempt.requestDigest,
212
+ introDigest: attempt.commitment.introDigest,
213
+ introWeight: attempt.commitment.introWeight,
214
+ senderPrincipalId: owner.principalId,
215
+ senderHelloLynkId: owner.hellolynkId,
216
+ senderDeviceId: owner.deviceId,
217
+ senderIdentityKeyId: owner.identityKeyId,
218
+ recipientPrincipalId: target.principalId,
219
+ recipientHelloLynkId: target.hellolynkId,
220
+ recipientDeviceId: safeId(handoff.recipient.deviceId, "human_relationship_contact_handoff_invalid"),
221
+ recipientIdentityKeyId: safeId(handoff.recipient.identity?.keyId, "human_relationship_contact_handoff_invalid"),
222
+ recipientPreKeyId: safeId(handoff.recipient.oneTimePreKey?.preKeyId, "human_relationship_contact_handoff_invalid"),
223
+ authorizationScope: CONTACT_REQUEST_E2EE_AUTHORIZATION_SCOPE,
224
+ intentSource: CONTACT_REQUEST_E2EE_INTENT_SOURCE,
225
+ messageType: CONTACT_REQUEST_E2EE_MESSAGE_TYPE,
226
+ createdAt,
227
+ expiresAt,
228
+ };
229
+ const envelope = await e2ee.sealContactRequestIntro({
230
+ protectedMetadata,
231
+ introDigestSalt: attempt.commitment.introDigestSalt,
232
+ recipientDeviceIdentity: handoff.recipient.identity,
233
+ recipientOneTimePreKey: handoff.recipient.oneTimePreKey,
234
+ introText: accepted.text,
235
+ });
236
+ attempt.phase = "relay_started";
237
+ try {
238
+ const relayed = await cloud.relayContactRequest({
239
+ attemptId: protectedMetadata.attemptId,
240
+ ...common,
241
+ claimId: safeId(handoff.claimId, "human_relationship_contact_handoff_invalid"),
242
+ envelopeDigest: digestContactRequestIntroEnvelope(envelope),
243
+ e2eeEnvelope: envelope,
244
+ runtimeHeaders: options.runtimeHeaders,
245
+ });
246
+ const status = new Set([
247
+ "contact_request_acknowledged",
248
+ "contact_request_queued",
249
+ ]).has(relayed.status)
250
+ ? "submitted"
251
+ : "delivery_unknown";
252
+ // Ciphertext retention is distinct from plaintext storage and delivery.
253
+ // A missing receipt field is unknown, never evidence of non-storage.
254
+ return finish(status, reserved?.quota,
255
+ typeof relayed.serverStoredCiphertext === "boolean" ? relayed.serverStoredCiphertext : null);
256
+ } catch (error) {
257
+ if (!Number.isInteger(error?.httpStatus) || error.httpStatus >= 500) {
258
+ return finish("delivery_unknown", reserved?.quota, null);
259
+ }
260
+ attempt.phase = "prepared";
261
+ throw error;
262
+ }
263
+ }
264
+ }
265
+
266
+ async function list(input = {}) {
267
+ record(input, "human_relationship_list_invalid");
268
+ if (Object.keys(input).some((key) => key !== "cursor")) {
269
+ throw failure("human_relationship_list_invalid");
270
+ }
271
+ const cursor = optionalCursor(input.cursor);
272
+ const cloudList = await cloud.listContactRequests(cursor == null ? {} : { cursor });
273
+ const cloudRequests = array(cloudList?.requests, "human_relationship_list_response_invalid");
274
+ const hasMore = cloudList?.hasMore == null ? false : cloudList.hasMore;
275
+ if (typeof hasMore !== "boolean") throw failure("human_relationship_list_response_invalid");
276
+ const nextCursor = optionalCursor(cloudList?.nextCursor);
277
+ if (hasMore !== (nextCursor != null)) {
278
+ throw failure("human_relationship_list_response_invalid");
279
+ }
280
+ const reconciledByRequestId = await reconcileUnresolvedDecisions();
281
+ const pending = await localStore.listPendingAckReceipts();
282
+ const cloudByAttempt = new Map(cloudRequests
283
+ .filter((item) => item.direction === "incoming" && item.hostBinding?.deliveryAttemptId)
284
+ .map((item) => [item.hostBinding.deliveryAttemptId, item]));
285
+ for (const receipt of pending) {
286
+ const remote = cloudByAttempt.get(receipt.attemptId);
287
+ if (!remote) continue;
288
+ await localStore.projectServerState({
289
+ localRecordId: receipt.localRecordId,
290
+ expectedLocalRecordDigest: receipt.localRecordDigest,
291
+ serverContactRequestId: safeId(remote.requestId, "human_relationship_list_response_invalid"),
292
+ state: publicState(remote.status),
293
+ serverUpdatedAt: safeTimestamp(remote.decidedAt || remote.createdAt),
294
+ });
295
+ }
296
+ const boundPending = await localStore.listForHuman();
297
+ const cloudByRequestId = new Map();
298
+ for (const item of cloudRequests) {
299
+ const requestId = safeId(item.requestId, "human_relationship_list_response_invalid");
300
+ const direction = oneOf(
301
+ item.direction,
302
+ ["incoming", "outgoing"],
303
+ "human_relationship_list_response_invalid",
304
+ );
305
+ const status = publicState(item.status);
306
+ if (direction !== "incoming") continue;
307
+ if (cloudByRequestId.has(requestId)) {
308
+ throw failure("human_relationship_list_response_invalid");
309
+ }
310
+ cloudByRequestId.set(requestId, { item, status });
311
+ }
312
+ for (const localRecord of boundPending) {
313
+ if (localRecord.serverContactRequestId == null
314
+ || localRecord.state !== "pending"
315
+ || localRecord.actionReady !== true
316
+ || localRecord.decisionOutcome != null
317
+ || localRecord.decisionAction != null) continue;
318
+ const remote = cloudByRequestId.get(localRecord.serverContactRequestId);
319
+ if (!remote || remote.status === "pending") continue;
320
+ let serverUpdatedAt;
321
+ try {
322
+ serverUpdatedAt = safeTimestamp(remote.item.decidedAt);
323
+ } catch {
324
+ throw failure("human_relationship_list_response_invalid");
325
+ }
326
+ try {
327
+ await localStore.projectServerState({
328
+ localRecordId: localRecord.localRecordId,
329
+ expectedLocalRecordDigest: localRecord.localRecordDigest,
330
+ serverContactRequestId: localRecord.serverContactRequestId,
331
+ state: remote.status,
332
+ serverUpdatedAt,
333
+ });
334
+ } catch (error) {
335
+ // A human decision may have acquired its durable marker after this
336
+ // list snapshot. In that race the marker wins and only the exact
337
+ // point-read path may settle it; never use the broad page as a receipt.
338
+ if (!new Set([
339
+ "contact_request_local_decision_unresolved",
340
+ "contact_request_local_decision_recovery_required",
341
+ "contact_request_local_state_conflict",
342
+ ]).has(error?.code)) throw error;
343
+ }
344
+ }
345
+ const local = await localStore.listForHuman();
346
+ const localByServerId = new Map(local
347
+ .filter((item) => item.serverContactRequestId)
348
+ .map((item) => [item.serverContactRequestId, item]));
349
+ const publicRequests = cloudRequests.map((item) => {
350
+ const requestId = safeId(item.requestId, "human_relationship_list_response_invalid");
351
+ const direction = oneOf(item.direction, ["incoming", "outgoing"], "human_relationship_list_response_invalid");
352
+ const cloudStatus = publicState(item.status);
353
+ const localRecord = localByServerId.get(requestId);
354
+ const reconciliation = reconciledByRequestId.get(requestId) || null;
355
+ const status = reconciledCloudState({
356
+ cloudStatus,
357
+ localRecord,
358
+ reconciliation,
359
+ });
360
+ const localDecisionFields = decisionFieldsForCloudRecord({
361
+ direction,
362
+ status,
363
+ localRecord,
364
+ });
365
+ return Object.freeze({
366
+ requestId,
367
+ direction,
368
+ status,
369
+ ...localDecisionFields,
370
+ otherPrincipal: Object.freeze({
371
+ displayName: safeDisplayName(item.otherPrincipal?.displayName),
372
+ hellolynkId: safeOptionalHelloLynkId(item.otherPrincipal?.hellolynkId),
373
+ }),
374
+ introText: direction === "incoming" && localRecord ? localRecord.introText : null,
375
+ introWeight: direction === "incoming" && localRecord ? localRecord.introWeight : null,
376
+ createdAt: safeTimestamp(item.createdAt),
377
+ decidedAt: reconciliation?.decidedAt
378
+ ?? (item.decidedAt == null ? null : safeTimestamp(item.decidedAt)),
379
+ humanOnly: true,
380
+ modelVisibility: "none",
381
+ });
382
+ });
383
+ for (const request of publicRequests) {
384
+ if (request.direction === "incoming" && request.status === "blocked") {
385
+ await revokeContactAutonomy(request.otherPrincipal.hellolynkId);
386
+ }
387
+ }
388
+ return Object.freeze({
389
+ requests: Object.freeze(publicRequests),
390
+ hasMore,
391
+ nextCursor,
392
+ serverStoredIntro: false,
393
+ serverStoredCiphertext: false,
394
+ modelCalls: 0,
395
+ });
396
+ }
397
+
398
+ async function decide(input = {}) {
399
+ exactKeys(input, ["requestId", "action"], "human_relationship_decision_invalid");
400
+ const requestId = safeId(input.requestId, "human_relationship_decision_invalid");
401
+ const action = oneOf(
402
+ input.action,
403
+ ["accept", "reject", "block"],
404
+ "human_relationship_decision_invalid",
405
+ );
406
+ const before = await localStore.listForHuman();
407
+ const local = before.find((item) => item.serverContactRequestId === requestId) || null;
408
+ if (!local || local.state !== "pending" || local.actionReady !== true
409
+ || local.decisionOutcome != null) {
410
+ throw failure("human_relationship_decision_known_failed");
411
+ }
412
+ let decisionAttempt;
413
+ try {
414
+ decisionAttempt = await localStore.beginDecision({
415
+ localRecordId: local.localRecordId,
416
+ expectedLocalRecordDigest: local.localRecordDigest,
417
+ serverContactRequestId: requestId,
418
+ action,
419
+ });
420
+ } catch (error) {
421
+ throw failure("human_relationship_decision_known_failed", {
422
+ cause: error,
423
+ httpStatus: error?.status,
424
+ });
425
+ }
426
+
427
+ let result;
428
+ try {
429
+ result = await cloud.decideContactRequest({ requestId, action });
430
+ } catch (error) {
431
+ const reconciliation = await reconcileDecisionAttempt(decisionAttempt);
432
+ if (reconciliation.outcome === "applied") {
433
+ return publicDecisionResult(requestId, action, reconciliation);
434
+ }
435
+ if (reconciliation.outcome === "not_applied") {
436
+ throw failure("human_relationship_decision_not_applied", {
437
+ cause: error,
438
+ httpStatus: error?.httpStatus,
439
+ });
440
+ }
441
+ if (reconciliation.outcome === "pending" && isKnownRemoteFailure(error)) {
442
+ try {
443
+ await abortDecisionAttempt(decisionAttempt);
444
+ } catch (abortError) {
445
+ throw failure("human_relationship_decision_outcome_unknown", { cause: abortError });
446
+ }
447
+ throw failure("human_relationship_decision_known_failed", {
448
+ cause: error,
449
+ httpStatus: error.httpStatus,
450
+ });
451
+ }
452
+ throw failure("human_relationship_decision_outcome_unknown", { cause: error });
453
+ }
454
+
455
+ let authoritative;
456
+ try {
457
+ authoritative = normalizeDecisionSuccess(result, requestId, action);
458
+ } catch (error) {
459
+ const reconciliation = await reconcileDecisionAttempt(decisionAttempt);
460
+ if (reconciliation.outcome === "applied") {
461
+ return publicDecisionResult(requestId, action, reconciliation);
462
+ }
463
+ if (reconciliation.outcome === "not_applied") {
464
+ throw failure("human_relationship_decision_not_applied", { cause: error });
465
+ }
466
+ throw failure("human_relationship_decision_outcome_unknown", { cause: error });
467
+ }
468
+ try {
469
+ await localStore.resolveDecision({
470
+ localRecordId: decisionAttempt.localRecordId,
471
+ expectedLocalRecordDigest: decisionAttempt.localRecordDigest,
472
+ serverContactRequestId: decisionAttempt.serverContactRequestId,
473
+ decisionId: decisionAttempt.decisionId,
474
+ state: authoritative.state,
475
+ serverUpdatedAt: authoritative.decidedAt,
476
+ });
477
+ } catch (error) {
478
+ // A concurrent background point-read may have committed this same
479
+ // terminal receipt while the original cloud response was in flight.
480
+ // Accept only that exact durable result; never resend or reinterpret a
481
+ // different decision, missing record or failed local write as success.
482
+ const settled = error?.code === "contact_request_local_decision_cas_conflict"
483
+ ? (await localStore.listForHuman().catch(() => [])).find((item) => (
484
+ item.localRecordId === decisionAttempt.localRecordId
485
+ && item.localRecordDigest === decisionAttempt.localRecordDigest
486
+ && item.serverContactRequestId === requestId
487
+ && item.state === authoritative.state
488
+ && item.serverUpdatedAt === authoritative.decidedAt
489
+ && item.decisionOutcome == null && item.decisionAction == null
490
+ )) : null;
491
+ if (!settled) throw failure("human_relationship_decision_outcome_unknown", { cause: error });
492
+ }
493
+ if (action === "block") {
494
+ await revokeContactAutonomy(local.sender?.hellolynkId);
495
+ }
496
+ return publicDecisionResult(requestId, action, {
497
+ state: authoritative.state,
498
+ relationshipStatus: authoritative.relationshipStatus,
499
+ });
500
+ }
501
+
502
+ async function reconcileUnresolvedDecisions() {
503
+ const unresolved = array(
504
+ await localStore.listUnresolvedDecisions(),
505
+ "human_relationship_local_decision_list_invalid",
506
+ );
507
+ const reconciled = new Map();
508
+ for (const decisionAttempt of unresolved) {
509
+ const result = await reconcileDecisionAttempt(decisionAttempt);
510
+ if (result.state != null) {
511
+ reconciled.set(decisionAttempt.serverContactRequestId, result);
512
+ }
513
+ }
514
+ return reconciled;
515
+ }
516
+
517
+ async function reconcileDecisionAttempt(decisionAttempt) {
518
+ let authoritative;
519
+ try {
520
+ authoritative = normalizeDecisionPointState(
521
+ await cloud.getContactRequestDecisionState({
522
+ requestId: decisionAttempt.serverContactRequestId,
523
+ }),
524
+ decisionAttempt.serverContactRequestId,
525
+ );
526
+ } catch {
527
+ return Object.freeze({
528
+ outcome: "unknown", state: null, relationshipStatus: null, decidedAt: null,
529
+ });
530
+ }
531
+ const actualState = authoritative.state;
532
+ if (actualState === "pending") {
533
+ return Object.freeze({
534
+ outcome: "pending", state: "pending", relationshipStatus: null, decidedAt: null,
535
+ });
536
+ }
537
+ if (!DECISION_TERMINAL_STATES.has(actualState)) {
538
+ return Object.freeze({
539
+ outcome: "unknown", state: null, relationshipStatus: null, decidedAt: null,
540
+ });
541
+ }
542
+ try {
543
+ await finalizeDecisionAttempt(decisionAttempt, authoritative);
544
+ } catch {
545
+ return Object.freeze({
546
+ outcome: "unknown", state: null, relationshipStatus: null, decidedAt: null,
547
+ });
548
+ }
549
+ if (actualState === "blocked") {
550
+ await revokeBlockedRequestAutonomy(decisionAttempt.serverContactRequestId);
551
+ }
552
+ return Object.freeze({
553
+ outcome: actualState === DECISION_TARGET_STATES[decisionAttempt.action]
554
+ && (decisionAttempt.action !== "block"
555
+ || authoritative.requestDecisionAction === "block")
556
+ ? "applied"
557
+ : "not_applied",
558
+ state: actualState,
559
+ relationshipStatus: authoritative.relationshipStatus,
560
+ decidedAt: authoritative.decidedAt || authoritative.observedAt,
561
+ });
562
+ }
563
+
564
+ async function finalizeDecisionAttempt(decisionAttempt, authoritative) {
565
+ const common = {
566
+ localRecordId: decisionAttempt.localRecordId,
567
+ expectedLocalRecordDigest: decisionAttempt.localRecordDigest,
568
+ serverContactRequestId: decisionAttempt.serverContactRequestId,
569
+ state: authoritative.state,
570
+ serverUpdatedAt: authoritative.decidedAt || authoritative.observedAt,
571
+ };
572
+ if (decisionAttempt.recoveryRequired === true) {
573
+ return localStore.projectServerState(common);
574
+ }
575
+ return localStore.resolveDecision({
576
+ ...common,
577
+ decisionId: decisionAttempt.decisionId,
578
+ });
579
+ }
580
+
581
+ async function abortDecisionAttempt(decisionAttempt) {
582
+ return localStore.abortDecision({
583
+ localRecordId: decisionAttempt.localRecordId,
584
+ expectedLocalRecordDigest: decisionAttempt.localRecordDigest,
585
+ serverContactRequestId: decisionAttempt.serverContactRequestId,
586
+ decisionId: decisionAttempt.decisionId,
587
+ });
588
+ }
589
+
590
+ async function recoverDecision(input = {}) {
591
+ exactKeys(input, ["requestId"], "human_relationship_decision_recovery_invalid");
592
+ const requestId = safeId(input.requestId, "human_relationship_decision_recovery_invalid");
593
+ const local = (await localStore.listForHuman())
594
+ .find((item) => item.serverContactRequestId === requestId) || null;
595
+ if (!local || local.state !== "pending"
596
+ || !new Set(["unknown", "recovery-required"]).has(local.decisionOutcome)) {
597
+ throw failure("human_relationship_decision_recovery_not_required");
598
+ }
599
+ const decisionAttempt = (await localStore.listUnresolvedDecisions())
600
+ .find((item) => item.serverContactRequestId === requestId) || null;
601
+ if (!decisionAttempt) {
602
+ throw failure("human_relationship_decision_outcome_unknown");
603
+ }
604
+ let authoritative;
605
+ try {
606
+ authoritative = normalizeDecisionPointState(
607
+ await cloud.getContactRequestDecisionState({ requestId }),
608
+ requestId,
609
+ );
610
+ } catch (error) {
611
+ throw failure("human_relationship_decision_outcome_unknown", { cause: error });
612
+ }
613
+ if (authoritative.state !== "pending") {
614
+ try {
615
+ await finalizeDecisionAttempt(decisionAttempt, authoritative);
616
+ } catch (error) {
617
+ throw failure("human_relationship_decision_outcome_unknown", { cause: error });
618
+ }
619
+ if (authoritative.state === "blocked") {
620
+ await revokeBlockedRequestAutonomy(decisionAttempt.serverContactRequestId);
621
+ }
622
+ return publicRecoveryResult(requestId, authoritative.state, false);
623
+ }
624
+ try {
625
+ await localStore.recoverDecision({
626
+ localRecordId: decisionAttempt.localRecordId,
627
+ expectedLocalRecordDigest: decisionAttempt.localRecordDigest,
628
+ serverContactRequestId: decisionAttempt.serverContactRequestId,
629
+ decisionId: decisionAttempt.decisionId,
630
+ });
631
+ } catch (error) {
632
+ throw failure("human_relationship_decision_outcome_unknown", { cause: error });
633
+ }
634
+ return publicRecoveryResult(requestId, "pending", true);
635
+ }
636
+
637
+ async function clearResolved() {
638
+ const result = await localStore.clearResolvedForHuman();
639
+ const deletedCount = Number(result?.deletedCount);
640
+ if (!Number.isSafeInteger(deletedCount) || deletedCount < 0) {
641
+ throw failure("human_relationship_local_cleanup_invalid");
642
+ }
643
+ return Object.freeze({
644
+ deletedCount,
645
+ localOnly: true,
646
+ modelCalls: 0,
647
+ });
648
+ }
649
+
650
+ async function cancel(input = {}) {
651
+ requireMethods(cloud, ["cancelContactRequest"], "human_relationship_cancel_unavailable");
652
+ exactKeys(input, ["requestId"], "human_relationship_cancel_invalid");
653
+ const requestId = safeId(input.requestId, "human_relationship_cancel_invalid");
654
+ let result;
655
+ try {
656
+ result = await cloud.cancelContactRequest({ requestId });
657
+ } catch (error) {
658
+ if (isKnownRemoteFailure(error)) {
659
+ throw failure("human_relationship_cancel_known_failed", {
660
+ cause: error,
661
+ httpStatus: error.httpStatus,
662
+ });
663
+ }
664
+ throw failure("human_relationship_cancel_outcome_unknown", { cause: error });
665
+ }
666
+ const normalized = normalizeCancelSuccess(result, requestId);
667
+ return Object.freeze({ ...normalized, modelCalls: 0 });
668
+ }
669
+
670
+ async function listRelationships() {
671
+ return readRelationships({ revokeEndedAuthorization: true });
672
+ }
673
+
674
+ async function readRelationships({ revokeEndedAuthorization }) {
675
+ requireMethods(cloud, ["listRelationships"], "human_relationship_relationship_list_unavailable");
676
+ const result = record(
677
+ await cloud.listRelationships(),
678
+ "human_relationship_relationship_list_response_invalid",
679
+ );
680
+ exactKeys(result, ["ok", "relationships"], "human_relationship_relationship_list_response_invalid");
681
+ if (result.ok !== true) throw failure("human_relationship_relationship_list_response_invalid");
682
+ const relationships = array(
683
+ result.relationships,
684
+ "human_relationship_relationship_list_response_invalid",
685
+ ).map(normalizeRelationship);
686
+ if (revokeEndedAuthorization) {
687
+ for (const relationship of relationships) {
688
+ if (relationship.status === "ended") {
689
+ await revokeContactAutonomy(relationship.otherPrincipal.hellolynkId);
690
+ }
691
+ }
692
+ if (typeof contactAuthorizationStore?.synchronizeRelationships === "function") {
693
+ await contactAuthorizationStore.synchronizeRelationships({
694
+ principalId: owner.principalId,
695
+ hellolynkId: owner.hellolynkId,
696
+ relationships,
697
+ });
698
+ }
699
+ }
700
+ return Object.freeze({
701
+ relationships: Object.freeze(relationships),
702
+ modelCalls: 0,
703
+ });
704
+ }
705
+
706
+ async function assertActiveRelationship(input = {}) {
707
+ exactKeys(input, ["hellolynkId"], "human_relationship_active_relationship_check_invalid");
708
+ const hellolynkId = safeHelloLynkId(
709
+ input.hellolynkId,
710
+ "human_relationship_active_relationship_check_invalid",
711
+ );
712
+ let relationshipsResult;
713
+ let blocksResult;
714
+ try {
715
+ relationshipsResult = await readRelationships({ revokeEndedAuthorization: false });
716
+ // Read blocks second so an active relationship snapshot can never be
717
+ // accepted on the basis of an older, concurrently completed block page.
718
+ blocksResult = await listBlocks();
719
+ } catch (error) {
720
+ throw failure("human_relationship_active_relationship_check_unavailable", { cause: error });
721
+ }
722
+ const active = relationshipsResult.relationships.some((relationship) => (
723
+ relationship.status === "active"
724
+ && relationship.otherPrincipal.hellolynkId === hellolynkId
725
+ ));
726
+ const blocked = blocksResult.blocks.some((block) => (
727
+ block.status === "active"
728
+ && block.otherPrincipal.hellolynkId === hellolynkId
729
+ ));
730
+ if (!active || blocked) {
731
+ throw failure("human_relationship_active_relationship_required");
732
+ }
733
+ return Object.freeze({ active: true, modelCalls: 0 });
734
+ }
735
+
736
+ async function endRelationship(input = {}) {
737
+ requireMethods(cloud, ["endRelationship"], "human_relationship_relationship_end_unavailable");
738
+ exactKeys(input, ["relationshipId"], "human_relationship_relationship_end_invalid");
739
+ const relationshipId = safeId(
740
+ input.relationshipId,
741
+ "human_relationship_relationship_end_invalid",
742
+ );
743
+ let cloudResult;
744
+ try {
745
+ cloudResult = await cloud.endRelationship({ relationshipId });
746
+ } catch (error) {
747
+ if (isKnownRemoteFailure(error)) {
748
+ throw failure("human_relationship_relationship_end_known_failed", {
749
+ cause: error,
750
+ httpStatus: error.httpStatus,
751
+ });
752
+ }
753
+ throw failure("human_relationship_relationship_end_outcome_unknown", { cause: error });
754
+ }
755
+ const result = record(cloudResult, "human_relationship_relationship_end_response_invalid");
756
+ exactKeys(result, ["ok", "relationship"], "human_relationship_relationship_end_response_invalid");
757
+ if (result.ok !== true) throw failure("human_relationship_relationship_end_response_invalid");
758
+ const relationship = normalizeRelationship(result.relationship);
759
+ if (relationship.relationshipId !== relationshipId || relationship.status !== "ended") {
760
+ throw failure("human_relationship_relationship_end_response_invalid");
761
+ }
762
+ await revokeContactAutonomy(relationship.otherPrincipal.hellolynkId);
763
+ return Object.freeze({ relationship, localHistoryDeleted: false, modelCalls: 0 });
764
+ }
765
+
766
+ async function listBlocks() {
767
+ requireMethods(cloud, ["listBlocks"], "human_relationship_block_list_unavailable");
768
+ const result = record(await cloud.listBlocks(), "human_relationship_block_list_response_invalid");
769
+ exactKeys(result, ["ok", "blocks"], "human_relationship_block_list_response_invalid");
770
+ if (result.ok !== true) throw failure("human_relationship_block_list_response_invalid");
771
+ const blocks = array(
772
+ result.blocks,
773
+ "human_relationship_block_list_response_invalid",
774
+ ).map((item) => normalizeBlock(item, "active"));
775
+ for (const block of blocks) {
776
+ // The cloud list is authoritative and may reflect a block created on a
777
+ // different device or before this Host restarted. Repair any stale local
778
+ // delegate authorization before returning the visible block state.
779
+ await revokeContactAutonomy(block.otherPrincipal.hellolynkId);
780
+ }
781
+ return Object.freeze({
782
+ blocks: Object.freeze(blocks),
783
+ modelCalls: 0,
784
+ });
785
+ }
786
+
787
+ async function removeBlock(input = {}) {
788
+ requireMethods(cloud, ["removeBlock"], "human_relationship_block_remove_unavailable");
789
+ exactKeys(input, ["blockId"], "human_relationship_block_remove_invalid");
790
+ const blockId = safeId(input.blockId, "human_relationship_block_remove_invalid");
791
+ let cloudResult;
792
+ try {
793
+ cloudResult = await cloud.removeBlock({ blockId });
794
+ } catch (error) {
795
+ if (isKnownRemoteFailure(error)) {
796
+ throw failure("human_relationship_block_remove_known_failed", {
797
+ cause: error,
798
+ httpStatus: error.httpStatus,
799
+ });
800
+ }
801
+ throw failure("human_relationship_block_remove_outcome_unknown", { cause: error });
802
+ }
803
+ const result = record(cloudResult, "human_relationship_block_remove_response_invalid");
804
+ exactKeys(result, ["ok", "block"], "human_relationship_block_remove_response_invalid");
805
+ if (result.ok !== true) throw failure("human_relationship_block_remove_response_invalid");
806
+ const block = normalizeBlock(result.block, "removed");
807
+ if (block.blockId !== blockId) throw failure("human_relationship_block_remove_response_invalid");
808
+ return Object.freeze({
809
+ block,
810
+ relationshipRestored: false,
811
+ authorizationRestored: false,
812
+ modelCalls: 0,
813
+ });
814
+ }
815
+
816
+ return Object.freeze({
817
+ contractId: HELLOLYNK_HUMAN_RELATIONSHIP_CONTROLLER_CONTRACT,
818
+ search,
819
+ send,
820
+ list,
821
+ decide,
822
+ recoverDecision,
823
+ clearResolved,
824
+ cancel,
825
+ listRelationships,
826
+ assertActiveRelationship,
827
+ endRelationship,
828
+ listBlocks,
829
+ removeBlock,
830
+ });
831
+
832
+ async function revokeBlockedRequestAutonomy(requestId) {
833
+ if (!contactAuthorizationStore) return;
834
+ const local = (await localStore.listForHuman())
835
+ .find((item) => item.serverContactRequestId === requestId) || null;
836
+ await revokeContactAutonomy(local?.sender?.hellolynkId);
837
+ }
838
+
839
+ async function revokeContactAutonomy(hellolynkId) {
840
+ if (!contactAuthorizationStore || hellolynkId == null) return;
841
+ const acceptedHelloLynkId = safeHelloLynkId(
842
+ hellolynkId,
843
+ "human_relationship_contact_authorization_identity_invalid",
844
+ );
845
+ try {
846
+ await contactAuthorizationStore.revokeAutonomousMessagingByHelloLynkId(
847
+ acceptedHelloLynkId,
848
+ );
849
+ } catch (error) {
850
+ throw failure("human_relationship_local_authorization_revoke_failed", { cause: error });
851
+ }
852
+ }
853
+
854
+ function pruneTargets() {
855
+ const observedAt = now();
856
+ for (const [key, value] of targets) {
857
+ if (value.expiresAtMs <= observedAt) {
858
+ targets.delete(key);
859
+ sendAttempts.delete(key);
860
+ }
861
+ }
862
+ }
863
+
864
+ function trimTargets() {
865
+ while (targets.size > TARGET_CAPACITY) {
866
+ const oldest = targets.keys().next().value;
867
+ targets.delete(oldest);
868
+ sendAttempts.delete(oldest);
869
+ }
870
+ }
871
+ }
872
+
873
+ function validateHandoff(handoff, owner, target, identityKeyId) {
874
+ record(handoff.sender, "human_relationship_contact_handoff_invalid");
875
+ record(handoff.recipient, "human_relationship_contact_handoff_invalid");
876
+ if (handoff.sender.principalId !== owner.principalId
877
+ || handoff.sender.hellolynkId !== owner.hellolynkId
878
+ || handoff.sender.deviceId !== owner.deviceId
879
+ || handoff.sender.identity?.keyId !== identityKeyId
880
+ || handoff.recipient.principalId !== target.principalId
881
+ || handoff.recipient.hellolynkId !== target.hellolynkId) {
882
+ throw failure("human_relationship_contact_handoff_mismatch");
883
+ }
884
+ }
885
+
886
+ function publicSendResult(status, quota = null, serverStoredCiphertext = false) {
887
+ return Object.freeze({
888
+ status,
889
+ quota: quota ? Object.freeze({
890
+ limit: Number(quota.limit),
891
+ used: Number(quota.used),
892
+ remaining: Number(quota.remaining),
893
+ nextResetAt: String(quota.nextResetAt || ""),
894
+ }) : null,
895
+ serverStoredIntro: false,
896
+ serverStoredCiphertext,
897
+ modelCalls: 0,
898
+ });
899
+ }
900
+
901
+ function publicDirectoryQuota(value) {
902
+ const quota = record(value?.directorySearch, "human_relationship_search_response_invalid");
903
+ return Object.freeze({
904
+ limit: Number(quota.limit),
905
+ remaining: Number(quota.remaining),
906
+ resetsAt: safeTimestamp(quota.resetsAt),
907
+ });
908
+ }
909
+
910
+ function normalizeDecisionSuccess(value, requestId, action) {
911
+ const result = record(value?.result, "human_relationship_decision_response_invalid");
912
+ const required = new Set(["requestId", "action", "status", "idempotent", "decidedAt"]);
913
+ const allowed = new Set(required);
914
+ if (action === "accept") allowed.add("relationshipStatus");
915
+ if (action === "block") allowed.add("blockStatus");
916
+ if (Object.keys(result).some((key) => !allowed.has(key))
917
+ || [...required].some((key) => !Object.hasOwn(result, key))) {
918
+ throw failure("human_relationship_decision_response_invalid");
919
+ }
920
+ if (safeId(result.requestId, "human_relationship_decision_response_invalid") !== requestId
921
+ || oneOf(result.action, ["accept", "reject", "block"], "human_relationship_decision_response_invalid") !== action
922
+ || publicState(result.status) !== DECISION_TARGET_STATES[action]
923
+ || typeof result.idempotent !== "boolean") {
924
+ throw failure("human_relationship_decision_response_invalid");
925
+ }
926
+ const relationshipStatus = action === "accept"
927
+ ? oneOf(result.relationshipStatus, ["active"], "human_relationship_decision_response_invalid")
928
+ : null;
929
+ if (action === "block"
930
+ && oneOf(result.blockStatus, ["active"], "human_relationship_decision_response_invalid") !== "active") {
931
+ throw failure("human_relationship_decision_response_invalid");
932
+ }
933
+ return Object.freeze({
934
+ state: DECISION_TARGET_STATES[action],
935
+ decidedAt: safeTimestamp(result.decidedAt),
936
+ relationshipStatus,
937
+ });
938
+ }
939
+
940
+ function normalizeCancelSuccess(value, requestId) {
941
+ const result = record(value?.result, "human_relationship_cancel_response_invalid");
942
+ exactKeys(result, [
943
+ "requestId", "action", "status", "idempotent", "decidedAt",
944
+ ], "human_relationship_cancel_response_invalid");
945
+ if (safeId(result.requestId, "human_relationship_cancel_response_invalid") !== requestId
946
+ || result.action !== "cancel"
947
+ || result.status !== "cancelled"
948
+ || typeof result.idempotent !== "boolean") {
949
+ throw failure("human_relationship_cancel_response_invalid");
950
+ }
951
+ return Object.freeze({
952
+ requestId,
953
+ status: "cancelled",
954
+ idempotent: result.idempotent,
955
+ decidedAt: safeTimestamp(result.decidedAt),
956
+ });
957
+ }
958
+
959
+ function normalizeRelationship(value) {
960
+ const relationship = record(value, "human_relationship_relationship_response_invalid");
961
+ exactKeys(relationship, [
962
+ "relationshipId", "otherPrincipal", "status", "createdAt", "updatedAt", "endedAt",
963
+ ], "human_relationship_relationship_response_invalid");
964
+ return Object.freeze({
965
+ relationshipId: safeId(
966
+ relationship.relationshipId,
967
+ "human_relationship_relationship_response_invalid",
968
+ ),
969
+ otherPrincipal: normalizeOtherPrincipal(
970
+ relationship.otherPrincipal,
971
+ "human_relationship_relationship_response_invalid",
972
+ ),
973
+ status: oneOf(
974
+ relationship.status,
975
+ ["active", "ended"],
976
+ "human_relationship_relationship_response_invalid",
977
+ ),
978
+ createdAt: safeTimestamp(relationship.createdAt),
979
+ updatedAt: safeTimestamp(relationship.updatedAt),
980
+ endedAt: relationship.endedAt == null ? null : safeTimestamp(relationship.endedAt),
981
+ });
982
+ }
983
+
984
+ function normalizeBlock(value, expectedStatus) {
985
+ const block = record(value, "human_relationship_block_response_invalid");
986
+ const keys = expectedStatus === "removed"
987
+ ? ["blockId", "otherPrincipal", "status", "createdAt", "removedAt"]
988
+ : ["blockId", "otherPrincipal", "status", "createdAt"];
989
+ exactKeys(block, keys, "human_relationship_block_response_invalid");
990
+ if (block.status !== expectedStatus) throw failure("human_relationship_block_response_invalid");
991
+ return Object.freeze({
992
+ blockId: safeId(block.blockId, "human_relationship_block_response_invalid"),
993
+ otherPrincipal: normalizeOtherPrincipal(
994
+ block.otherPrincipal,
995
+ "human_relationship_block_response_invalid",
996
+ ),
997
+ status: expectedStatus,
998
+ createdAt: safeTimestamp(block.createdAt),
999
+ ...(expectedStatus === "removed" ? { removedAt: safeTimestamp(block.removedAt) } : {}),
1000
+ });
1001
+ }
1002
+
1003
+ function normalizeOtherPrincipal(value, code) {
1004
+ const principal = record(value, code);
1005
+ exactKeys(principal, ["displayName", "hellolynkId", "hellolynkIdPurpose"], code);
1006
+ if (principal.hellolynkIdPurpose !== "human_identity_verification_only") throw failure(code);
1007
+ return Object.freeze({
1008
+ displayName: safeDisplayName(principal.displayName),
1009
+ hellolynkId: safeHelloLynkId(principal.hellolynkId, code),
1010
+ });
1011
+ }
1012
+
1013
+ function normalizeDecisionPointState(value, requestId) {
1014
+ const result = record(value, "human_relationship_decision_state_invalid");
1015
+ exactKeys(result, [
1016
+ "ok",
1017
+ "requestId",
1018
+ "status",
1019
+ "decidedAt",
1020
+ "requestDecisionAction",
1021
+ "viewerBlockStatus",
1022
+ "relationshipStatus",
1023
+ "observedAt",
1024
+ ], "human_relationship_decision_state_invalid");
1025
+ if (result.ok !== true
1026
+ || safeId(result.requestId, "human_relationship_decision_state_invalid") !== requestId) {
1027
+ throw failure("human_relationship_decision_state_invalid");
1028
+ }
1029
+ const state = publicState(result.status);
1030
+ const requestDecisionAction = result.requestDecisionAction == null
1031
+ ? null
1032
+ : oneOf(
1033
+ result.requestDecisionAction,
1034
+ ["block"],
1035
+ "human_relationship_decision_state_invalid",
1036
+ );
1037
+ if ((state === "blocked") !== (requestDecisionAction === "block")) {
1038
+ throw failure("human_relationship_decision_state_invalid");
1039
+ }
1040
+ const decidedAt = result.decidedAt == null ? null : safeTimestamp(result.decidedAt);
1041
+ if (state !== "pending" && decidedAt == null) {
1042
+ throw failure("human_relationship_decision_state_invalid");
1043
+ }
1044
+ return Object.freeze({
1045
+ state,
1046
+ decidedAt,
1047
+ requestDecisionAction,
1048
+ viewerBlockStatus: oneOf(
1049
+ result.viewerBlockStatus,
1050
+ ["active", "none"],
1051
+ "human_relationship_decision_state_invalid",
1052
+ ),
1053
+ relationshipStatus: result.relationshipStatus == null
1054
+ ? null
1055
+ : oneOf(result.relationshipStatus, ["active", "ended"], "human_relationship_decision_state_invalid"),
1056
+ observedAt: safeTimestamp(result.observedAt),
1057
+ });
1058
+ }
1059
+
1060
+ function publicDecisionResult(requestId, action, result) {
1061
+ return Object.freeze({
1062
+ requestId,
1063
+ action,
1064
+ status: result.state,
1065
+ relationshipStatus: result.relationshipStatus === "active" ? "active" : null,
1066
+ modelCalls: 0,
1067
+ });
1068
+ }
1069
+
1070
+ function publicRecoveryResult(requestId, status, recovered) {
1071
+ return Object.freeze({
1072
+ requestId,
1073
+ status,
1074
+ recovered,
1075
+ requiresNewDecision: recovered,
1076
+ remoteDecisionWrites: 0,
1077
+ modelCalls: 0,
1078
+ });
1079
+ }
1080
+
1081
+ function isKnownRemoteFailure(error) {
1082
+ return Number.isInteger(error?.httpStatus)
1083
+ && error.httpStatus >= 400
1084
+ && error.httpStatus < 500;
1085
+ }
1086
+
1087
+ function validateIntro(value) {
1088
+ try {
1089
+ return validateContactRequestIntro(value);
1090
+ } catch (error) {
1091
+ throw failure(String(error?.code || "contact_request_intro_invalid"), { cause: error });
1092
+ }
1093
+ }
1094
+
1095
+ function publicState(value) {
1096
+ const state = String(value || "");
1097
+ if (state === "cancelled_by_block") return "cancelled";
1098
+ if (state === "cancelled_by_sender" || state === "superseded") {
1099
+ return "cancelled";
1100
+ }
1101
+ return oneOf(state, ["pending", "accepted", "rejected", "cancelled", "blocked", "expired"],
1102
+ "human_relationship_state_invalid");
1103
+ }
1104
+
1105
+ function reconciledCloudState({ cloudStatus, localRecord, reconciliation }) {
1106
+ if (reconciliation?.state != null) return reconciliation.state;
1107
+ if (localRecord?.state === "pending"
1108
+ && new Set(["unknown", "recovery-required"]).has(localRecord.decisionOutcome)) {
1109
+ return "pending";
1110
+ }
1111
+ if (cloudStatus === "pending" && localRecord?.state != null
1112
+ && localRecord.state !== "pending") return localRecord.state;
1113
+ return cloudStatus;
1114
+ }
1115
+
1116
+ function decisionFieldsForCloudRecord({ direction, status, localRecord }) {
1117
+ if (direction !== "incoming") {
1118
+ return Object.freeze({
1119
+ actionReady: false,
1120
+ decisionOutcome: null,
1121
+ decisionAction: null,
1122
+ });
1123
+ }
1124
+ const consistent = localRecord
1125
+ && localRecord.state === status
1126
+ && typeof localRecord.actionReady === "boolean"
1127
+ && new Set([null, "unknown", "recovery-required"]).has(localRecord.decisionOutcome)
1128
+ && (localRecord.decisionAction == null
1129
+ || new Set(["accept", "reject", "block"]).has(localRecord.decisionAction))
1130
+ && ((localRecord.decisionOutcome === "unknown")
1131
+ === (localRecord.decisionAction != null));
1132
+ if (!consistent) {
1133
+ return Object.freeze({
1134
+ actionReady: false,
1135
+ decisionOutcome: null,
1136
+ decisionAction: null,
1137
+ });
1138
+ }
1139
+ const actionReady = status === "pending"
1140
+ && localRecord.actionReady === true
1141
+ && localRecord.decisionOutcome == null
1142
+ && localRecord.decisionAction == null;
1143
+ const decisionOutcome = localRecord.decisionOutcome;
1144
+ const decisionAction = decisionOutcome === "unknown" ? localRecord.decisionAction : null;
1145
+ if ((decisionOutcome === "unknown") !== (decisionAction != null)
1146
+ || (decisionOutcome === "recovery-required" && decisionAction != null)) {
1147
+ return Object.freeze({
1148
+ actionReady: false,
1149
+ decisionOutcome: null,
1150
+ decisionAction: null,
1151
+ });
1152
+ }
1153
+ return Object.freeze({ actionReady, decisionOutcome, decisionAction });
1154
+ }
1155
+
1156
+ function requireMethods(value, methods, code) {
1157
+ if (!value || methods.some((method) => typeof value[method] !== "function")) throw failure(code);
1158
+ return value;
1159
+ }
1160
+
1161
+ function exactKeys(value, keys, code) {
1162
+ record(value, code);
1163
+ const actual = Object.keys(value).sort();
1164
+ const expected = [...keys].sort();
1165
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
1166
+ throw failure(code);
1167
+ }
1168
+ }
1169
+
1170
+ function record(value, code) {
1171
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw failure(code);
1172
+ return value;
1173
+ }
1174
+
1175
+ function array(value, code) {
1176
+ if (!Array.isArray(value)) throw failure(code);
1177
+ return value;
1178
+ }
1179
+
1180
+ function safeId(value, code) {
1181
+ const text = String(value || "");
1182
+ if (!SAFE_ID.test(text)) throw failure(code);
1183
+ return text;
1184
+ }
1185
+
1186
+ function safeHelloLynkId(value, code) {
1187
+ const text = String(value || "");
1188
+ if (!HELLOLYNK_ID.test(text)) throw failure(code);
1189
+ return text;
1190
+ }
1191
+
1192
+ function safeOptionalHelloLynkId(value) {
1193
+ return value == null ? null : safeHelloLynkId(value, "human_relationship_list_response_invalid");
1194
+ }
1195
+
1196
+ function safeDisplayName(value) {
1197
+ if (value == null) return null;
1198
+ const text = String(value);
1199
+ if (!text || text.length > 80 || /[\u0000-\u001f\u007f-\u009f]/u.test(text)) {
1200
+ throw failure("human_relationship_display_name_invalid");
1201
+ }
1202
+ return text;
1203
+ }
1204
+
1205
+ function safeTimestamp(value) {
1206
+ const text = String(value || "");
1207
+ if (!Number.isFinite(Date.parse(text))) throw failure("human_relationship_timestamp_invalid");
1208
+ return text;
1209
+ }
1210
+
1211
+ function optionalCursor(value) {
1212
+ if (value == null) return null;
1213
+ const cursor = String(value);
1214
+ if (!/^[A-Za-z0-9_-]{20,512}$/.test(cursor)) {
1215
+ throw failure("human_relationship_list_response_invalid");
1216
+ }
1217
+ return cursor;
1218
+ }
1219
+
1220
+ function nonEmpty(value, code) {
1221
+ const text = String(value || "");
1222
+ if (!text || text.length > 4_096) throw failure(code);
1223
+ return text;
1224
+ }
1225
+
1226
+ function oneOf(value, allowed, code) {
1227
+ const text = String(value || "");
1228
+ if (!allowed.includes(text)) throw failure(code);
1229
+ return text;
1230
+ }
1231
+
1232
+ function failure(code, options = {}) {
1233
+ return new HelloLynkHumanRelationshipControllerError(code, options);
1234
+ }