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

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 +101 -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 +277 -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,867 @@
1
+ import {
2
+ createCipheriv,
3
+ createDecipheriv,
4
+ createHash,
5
+ createPrivateKey,
6
+ createPublicKey,
7
+ diffieHellman,
8
+ generateKeyPairSync,
9
+ hkdfSync,
10
+ randomBytes,
11
+ sign,
12
+ timingSafeEqual,
13
+ verify,
14
+ } from "node:crypto";
15
+
16
+ import {
17
+ DIRECT_TEXT_E2EE_KEY_SCHEMA_VERSION,
18
+ publicDirectTextDeviceIdentity,
19
+ publicOneTimePreKey,
20
+ verifyDeviceIdentityRegistration,
21
+ verifyOneTimePreKeyRegistration,
22
+ } from "./hellolynk-direct-text-e2ee.mjs";
23
+
24
+ export const CONTACT_REQUEST_E2EE_SCHEMA_VERSION = 3;
25
+ export const CONTACT_REQUEST_E2EE_PROTOCOL = "hellolynk_contact_request_intro_e2ee_v3";
26
+ export const CONTACT_REQUEST_E2EE_ALGORITHM = "X25519-HKDF-SHA256-AES-256-GCM-Ed25519";
27
+ export const CONTACT_REQUEST_E2EE_AUTHORIZATION_SCOPE = "contact.request.send";
28
+ export const CONTACT_REQUEST_E2EE_INTENT_SOURCE = "hellolynk_host_explicit_human_ui";
29
+ export const CONTACT_REQUEST_E2EE_MESSAGE_TYPE = "contact_request_intro";
30
+ export const CONTACT_REQUEST_E2EE_MAX_WEIGHT = 40;
31
+ export const CONTACT_REQUEST_E2EE_MAX_TTL_MS = 7 * 24 * 60 * 60 * 1000;
32
+
33
+ const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
34
+ const INTRO_DIGEST_SALT_BYTES = 16;
35
+ const MAX_CIPHERTEXT_BYTES = INTRO_DIGEST_SALT_BYTES + 80;
36
+ // Keep the local record commitment stable. Its random salt is private in v3,
37
+ // carried only inside the encrypted payload, never in relay metadata.
38
+ const INTRO_DIGEST_DOMAIN = Buffer.from("hellolynk/contact-request/intro-digest/v2\0", "utf8");
39
+ const ENVELOPE_SIGNATURE_DOMAIN = "hellolynk/contact-request/envelope-signature/v3\0";
40
+ const ENVELOPE_AAD_DOMAIN = "hellolynk/contact-request/envelope-aad/v3\0";
41
+ // Replay identity is independent of the wire encryption version. Preserve
42
+ // existing local deduplication records when the envelope format changes.
43
+ const REPLAY_KEY_DOMAIN = "hellolynk/contact-request/replay-key/v2\0";
44
+ const HKDF_INFO = Buffer.from("hellolynk/contact-request/e2ee/v3/aes-256-gcm", "utf8");
45
+ const textDecoder = new TextDecoder("utf-8", { fatal: true });
46
+
47
+ const PROTECTED_FIELDS = Object.freeze([
48
+ "attemptId",
49
+ "clientRequestId",
50
+ "requestDigest",
51
+ "introDigest",
52
+ "introWeight",
53
+ "senderPrincipalId",
54
+ "senderHelloLynkId",
55
+ "senderDeviceId",
56
+ "senderIdentityKeyId",
57
+ "recipientPrincipalId",
58
+ "recipientHelloLynkId",
59
+ "recipientDeviceId",
60
+ "recipientIdentityKeyId",
61
+ "recipientPreKeyId",
62
+ "authorizationScope",
63
+ "intentSource",
64
+ "messageType",
65
+ "createdAt",
66
+ "expiresAt",
67
+ ]);
68
+ const ENVELOPE_FIELDS = Object.freeze([
69
+ "schemaVersion",
70
+ "protocol",
71
+ "algorithm",
72
+ "protected",
73
+ "ephemeralPublicKey",
74
+ "kdfSalt",
75
+ "iv",
76
+ "ciphertext",
77
+ "authenticationTag",
78
+ "signature",
79
+ ]);
80
+ const EXPECTED_ROUTE_FIELDS = Object.freeze(["principalId", "hellolynkId", "deviceId"]);
81
+ const COMMITMENT_OPTION_FIELDS = Object.freeze(["introDigestSalt"]);
82
+ const PUBLIC_VERIFICATION_INPUT_FIELDS = Object.freeze([
83
+ "envelope",
84
+ "senderDeviceIdentity",
85
+ "recipientDeviceIdentity",
86
+ "recipientOneTimePreKey",
87
+ "expectedSender",
88
+ "expectedRecipient",
89
+ ]);
90
+
91
+ const FORBIDDEN_CODE_POINT = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u;
92
+ // A colon in a name or sentence is not itself a link (for example Alice:你好).
93
+ const URI_SCHEME = /\b(?:[A-Za-z][A-Za-z0-9+.-]{0,31}:[/\\]|(?:https?|ftps?|wss?|file|mailto|tel|sms|data|javascript|vbscript|blob|about|intent|magnet|urn|hellolynk):\S)/iu;
94
+ const EMAIL_ADDRESS = /[\p{L}\p{N}\p{M}.!#$%&'*+/=?^_`{|}~-]+@[\p{L}\p{N}\p{M}](?:[\p{L}\p{N}\p{M}-]{0,61}[\p{L}\p{N}\p{M}])?(?:\.[\p{L}\p{N}\p{M}](?:[\p{L}\p{N}\p{M}-]{0,61}[\p{L}\p{N}\p{M}])?)*/iu;
95
+ const ASCII_DOMAIN = /(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{2,63}|xn--[a-z0-9-]{2,59})(?=$|[^a-z0-9-])/iu;
96
+ const UNICODE_DOMAIN = /(?:[\p{L}\p{N}](?:[\p{L}\p{N}\p{M}-]{0,61}[\p{L}\p{N}\p{M}])?\.)+(?=[\p{L}\p{N}\p{M}-]{2,63}(?=$|[^\p{L}\p{N}\p{M}-]))\p{L}(?:[\p{L}\p{N}\p{M}-]{0,61}[\p{L}\p{N}\p{M}])?(?=$|[^\p{L}\p{N}\p{M}-])/iu;
97
+ const WWW_LINK = /\bwww\s*\./iu;
98
+ const IPV4_LINK = /(?:^|[^0-9])(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?=$|[^0-9])/u;
99
+
100
+ export class HelloLynkContactRequestE2eeError extends Error {
101
+ constructor(code) {
102
+ super(code);
103
+ this.name = "HelloLynkContactRequestE2eeError";
104
+ this.code = code;
105
+ }
106
+ }
107
+
108
+ export function validateContactRequestIntro(value) {
109
+ if (typeof value !== "string") {
110
+ throw new HelloLynkContactRequestE2eeError("contact_request_intro_invalid");
111
+ }
112
+ if (value.length > CONTACT_REQUEST_E2EE_MAX_WEIGHT * 2) {
113
+ throw new HelloLynkContactRequestE2eeError("contact_request_intro_too_long");
114
+ }
115
+ if (FORBIDDEN_CODE_POINT.test(value)) {
116
+ throw new HelloLynkContactRequestE2eeError("contact_request_intro_forbidden_character");
117
+ }
118
+ if (/^\p{Zs}*$/u.test(value)) return Object.freeze({ text: "", introWeight: 0 });
119
+ let introWeight = 0;
120
+ for (const codePoint of value) {
121
+ const scalar = codePoint.codePointAt(0);
122
+ introWeight += scalar >= 0x20 && scalar <= 0x7e ? 1 : 2;
123
+ if (introWeight > CONTACT_REQUEST_E2EE_MAX_WEIGHT) {
124
+ throw new HelloLynkContactRequestE2eeError("contact_request_intro_too_long");
125
+ }
126
+ }
127
+ const linkCandidate = value.normalize("NFKC").replace(/[\u3002\uFF0E\uFF61]/gu, ".");
128
+ if (
129
+ URI_SCHEME.test(linkCandidate)
130
+ || EMAIL_ADDRESS.test(linkCandidate)
131
+ || ASCII_DOMAIN.test(linkCandidate)
132
+ || UNICODE_DOMAIN.test(linkCandidate)
133
+ || WWW_LINK.test(linkCandidate)
134
+ || IPV4_LINK.test(linkCandidate)
135
+ ) {
136
+ throw new HelloLynkContactRequestE2eeError("contact_request_intro_link_forbidden");
137
+ }
138
+ return Object.freeze({ text: value, introWeight });
139
+ }
140
+
141
+ export function createContactRequestIntroCommitment(text, options = {}) {
142
+ assertPlainRecord(options, "contact_request_intro_commitment_options_invalid");
143
+ assertOnlyKnownFields(options, COMMITMENT_OPTION_FIELDS, "contact_request_intro_commitment_options_invalid");
144
+ const accepted = validateContactRequestIntro(text);
145
+ const salt = Object.hasOwn(options, "introDigestSalt")
146
+ ? decodeBase64Url(
147
+ options.introDigestSalt,
148
+ INTRO_DIGEST_SALT_BYTES,
149
+ "contact_request_intro_digest_salt_invalid",
150
+ )
151
+ : randomBytes(INTRO_DIGEST_SALT_BYTES);
152
+ const introDigestSalt = salt.toString("base64url");
153
+ const introDigest = contactRequestIntroDigest(accepted.text, salt);
154
+ salt.fill(0);
155
+ return Object.freeze({
156
+ introDigest,
157
+ introDigestSalt,
158
+ introWeight: accepted.introWeight,
159
+ });
160
+ }
161
+
162
+ export function createContactRequestRecordDigest(input) {
163
+ assertExactFields(
164
+ input,
165
+ ["recipientPrincipalId", "introDigest", "introWeight"],
166
+ "contact_request_record_digest_input_invalid",
167
+ );
168
+ const recipientPrincipalId = assertSafeId(
169
+ input.recipientPrincipalId,
170
+ "contact_request_recipient_principal_id_invalid",
171
+ );
172
+ const introDigest = assertDigest(input.introDigest, "contact_request_intro_digest_invalid");
173
+ const introWeight = input.introWeight;
174
+ if (!Number.isSafeInteger(introWeight)
175
+ || introWeight < 0
176
+ || introWeight > CONTACT_REQUEST_E2EE_MAX_WEIGHT) {
177
+ throw new HelloLynkContactRequestE2eeError("contact_request_intro_weight_invalid");
178
+ }
179
+ return createHash("sha256")
180
+ .update(
181
+ `hellolynk-contact-request-record-v2\0${recipientPrincipalId}\0${introDigest}\0${introWeight}`,
182
+ "utf8",
183
+ )
184
+ .digest("hex");
185
+ }
186
+
187
+ export function sealContactRequestIntroEnvelope(input, options = {}) {
188
+ assertExactFields(input, [
189
+ "protectedMetadata",
190
+ "recipientDeviceIdentity",
191
+ "recipientOneTimePreKey",
192
+ "senderDeviceKeyBundle",
193
+ "introText",
194
+ "introDigestSalt",
195
+ ], "contact_request_e2ee_seal_input_invalid");
196
+ const nowMs = normalizeNowOptions(options, "contact_request_e2ee_seal_options_invalid");
197
+ const protectedMetadata = canonicalProtectedMetadata(input.protectedMetadata);
198
+ assertEnvelopeTime(protectedMetadata, nowMs);
199
+
200
+ const senderIdentity = publicIdentityFromPrivateBundle(input.senderDeviceKeyBundle);
201
+ const recipientIdentity = canonicalDeviceIdentity(input.recipientDeviceIdentity);
202
+ const recipientPreKey = canonicalPublicPreKey(input.recipientOneTimePreKey);
203
+ if (!verifyDeviceIdentityRegistration(recipientIdentity)) {
204
+ throw new HelloLynkContactRequestE2eeError("contact_request_recipient_identity_signature_invalid");
205
+ }
206
+ if (!verifyOneTimePreKeyRegistration({
207
+ identity: recipientIdentity,
208
+ oneTimePreKey: recipientPreKey,
209
+ })) {
210
+ throw new HelloLynkContactRequestE2eeError("contact_request_recipient_prekey_signature_invalid");
211
+ }
212
+ if (protectedMetadata.senderIdentityKeyId !== senderIdentity.keyId) {
213
+ throw new HelloLynkContactRequestE2eeError("contact_request_sender_identity_key_mismatch");
214
+ }
215
+ if (protectedMetadata.recipientIdentityKeyId !== recipientIdentity.keyId) {
216
+ throw new HelloLynkContactRequestE2eeError("contact_request_recipient_identity_key_mismatch");
217
+ }
218
+ if (protectedMetadata.recipientPreKeyId !== recipientPreKey.preKeyId) {
219
+ throw new HelloLynkContactRequestE2eeError("contact_request_recipient_prekey_mismatch");
220
+ }
221
+
222
+ const acceptedIntro = validateContactRequestIntro(input.introText);
223
+ verifyIntroCommitment(protectedMetadata, acceptedIntro, input.introDigestSalt);
224
+ const privateSalt = decodeBase64Url(input.introDigestSalt, INTRO_DIGEST_SALT_BYTES,
225
+ "contact_request_intro_digest_salt_invalid");
226
+ const plaintext = Buffer.concat([privateSalt, Buffer.from(acceptedIntro.text, "utf8")]);
227
+ privateSalt.fill(0);
228
+ const { privateKey: ephemeralPrivateKey, publicKey: ephemeralPublicKey } = generateKeyPairSync("x25519");
229
+ const ephemeralPublicKeyValue = ephemeralPublicKey
230
+ .export({ format: "der", type: "spki" })
231
+ .toString("base64url");
232
+ const kdfSalt = randomBytes(32);
233
+ const iv = randomBytes(12);
234
+ const header = Object.freeze({
235
+ protected: protectedMetadata,
236
+ ephemeralPublicKey: ephemeralPublicKeyValue,
237
+ kdfSalt: kdfSalt.toString("base64url"),
238
+ iv: iv.toString("base64url"),
239
+ });
240
+ const aad = domainSeparatedBytes(ENVELOPE_AAD_DOMAIN, header);
241
+ const recipientPublicKey = importPublicDer(
242
+ recipientPreKey.publicKey,
243
+ "x25519",
244
+ "contact_request_recipient_prekey_public_invalid",
245
+ );
246
+ let sharedSecret;
247
+ let aesKey;
248
+ try {
249
+ sharedSecret = diffieHellman({ privateKey: ephemeralPrivateKey, publicKey: recipientPublicKey });
250
+ aesKey = Buffer.from(hkdfSync("sha256", sharedSecret, kdfSalt, HKDF_INFO, 32));
251
+ const cipher = createCipheriv("aes-256-gcm", aesKey, iv, { authTagLength: 16 });
252
+ cipher.setAAD(aad, { plaintextLength: plaintext.length });
253
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
254
+ const unsignedEnvelope = canonicalUnsignedEnvelope({
255
+ schemaVersion: CONTACT_REQUEST_E2EE_SCHEMA_VERSION,
256
+ protocol: CONTACT_REQUEST_E2EE_PROTOCOL,
257
+ algorithm: CONTACT_REQUEST_E2EE_ALGORITHM,
258
+ protected: protectedMetadata,
259
+ ephemeralPublicKey: header.ephemeralPublicKey,
260
+ kdfSalt: header.kdfSalt,
261
+ iv: header.iv,
262
+ ciphertext: ciphertext.toString("base64url"),
263
+ authenticationTag: cipher.getAuthTag().toString("base64url"),
264
+ });
265
+ const signingPrivateKey = privateIdentityKey(input.senderDeviceKeyBundle);
266
+ return Object.freeze({
267
+ ...unsignedEnvelope,
268
+ signature: sign(
269
+ null,
270
+ domainSeparatedBytes(ENVELOPE_SIGNATURE_DOMAIN, unsignedEnvelope),
271
+ signingPrivateKey,
272
+ ).toString("base64url"),
273
+ });
274
+ } catch (error) {
275
+ if (error instanceof HelloLynkContactRequestE2eeError) throw error;
276
+ throw new HelloLynkContactRequestE2eeError("contact_request_encryption_failed");
277
+ } finally {
278
+ plaintext.fill(0);
279
+ kdfSalt.fill(0);
280
+ iv.fill(0);
281
+ sharedSecret?.fill(0);
282
+ aesKey?.fill(0);
283
+ }
284
+ }
285
+
286
+ export function openContactRequestIntroEnvelope(input, options = {}) {
287
+ assertExactFields(input, [
288
+ "envelope",
289
+ "senderDeviceIdentity",
290
+ "recipientDeviceKeyBundle",
291
+ "recipientOneTimePreKey",
292
+ "expectedSender",
293
+ "expectedRecipient",
294
+ ], "contact_request_e2ee_open_input_invalid");
295
+ const nowMs = normalizeNowOptions(options, "contact_request_e2ee_open_options_invalid");
296
+ const recipientIdentity = publicIdentityFromPrivateBundle(input.recipientDeviceKeyBundle);
297
+ const recipientPreKey = privatePreKeyFromBundle(
298
+ input.recipientOneTimePreKey,
299
+ input.recipientDeviceKeyBundle,
300
+ );
301
+ const verified = verifyContactRequestIntroEnvelopePublicBinding({
302
+ envelope: input.envelope,
303
+ senderDeviceIdentity: input.senderDeviceIdentity,
304
+ recipientDeviceIdentity: recipientIdentity,
305
+ recipientOneTimePreKey: recipientPreKey.publicPreKey,
306
+ expectedSender: input.expectedSender,
307
+ expectedRecipient: input.expectedRecipient,
308
+ }, { nowMs });
309
+ const { envelope, protectedMetadata } = verified;
310
+
311
+ const ephemeralPublicKey = importPublicDer(
312
+ envelope.ephemeralPublicKey,
313
+ "x25519",
314
+ "contact_request_ephemeral_public_key_invalid",
315
+ );
316
+ const recipientPrivateKey = importPrivateDer(
317
+ recipientPreKey.privateKey,
318
+ "x25519",
319
+ "contact_request_recipient_prekey_private_invalid",
320
+ );
321
+ const kdfSalt = decodeBase64Url(envelope.kdfSalt, 32, "contact_request_kdf_salt_invalid");
322
+ const iv = decodeBase64Url(envelope.iv, 12, "contact_request_iv_invalid");
323
+ const ciphertext = decodeBase64Url(
324
+ envelope.ciphertext,
325
+ null,
326
+ "contact_request_ciphertext_invalid",
327
+ { minimumBytes: INTRO_DIGEST_SALT_BYTES, maximumBytes: MAX_CIPHERTEXT_BYTES },
328
+ );
329
+ const authenticationTag = decodeBase64Url(
330
+ envelope.authenticationTag,
331
+ 16,
332
+ "contact_request_authentication_tag_invalid",
333
+ );
334
+ const aad = domainSeparatedBytes(ENVELOPE_AAD_DOMAIN, {
335
+ protected: protectedMetadata,
336
+ ephemeralPublicKey: envelope.ephemeralPublicKey,
337
+ kdfSalt: envelope.kdfSalt,
338
+ iv: envelope.iv,
339
+ });
340
+ let sharedSecret;
341
+ let aesKey;
342
+ let plaintext;
343
+ try {
344
+ sharedSecret = diffieHellman({ privateKey: recipientPrivateKey, publicKey: ephemeralPublicKey });
345
+ aesKey = Buffer.from(hkdfSync("sha256", sharedSecret, kdfSalt, HKDF_INFO, 32));
346
+ const decipher = createDecipheriv("aes-256-gcm", aesKey, iv, { authTagLength: 16 });
347
+ decipher.setAAD(aad, { plaintextLength: ciphertext.length });
348
+ decipher.setAuthTag(authenticationTag);
349
+ plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
350
+ } catch {
351
+ throw new HelloLynkContactRequestE2eeError("contact_request_decryption_failed");
352
+ } finally {
353
+ kdfSalt.fill(0);
354
+ iv.fill(0);
355
+ sharedSecret?.fill(0);
356
+ aesKey?.fill(0);
357
+ }
358
+
359
+ try {
360
+ const introDigestSalt = plaintext.subarray(0, INTRO_DIGEST_SALT_BYTES).toString("base64url");
361
+ let introText;
362
+ try {
363
+ introText = textDecoder.decode(plaintext.subarray(INTRO_DIGEST_SALT_BYTES));
364
+ } catch {
365
+ throw new HelloLynkContactRequestE2eeError("contact_request_plaintext_invalid");
366
+ }
367
+ const acceptedIntro = validateContactRequestIntro(introText);
368
+ verifyIntroCommitment(protectedMetadata, acceptedIntro, introDigestSalt);
369
+ return Object.freeze({
370
+ introText: acceptedIntro.text,
371
+ introDigestSalt,
372
+ protectedMetadata,
373
+ envelopeDigest: digestContactRequestIntroEnvelope(envelope),
374
+ replayKey: contactRequestIntroReplayKey(protectedMetadata),
375
+ });
376
+ } finally {
377
+ plaintext.fill(0);
378
+ }
379
+ }
380
+
381
+ export function verifyContactRequestIntroEnvelopePublicBinding(input, options = {}) {
382
+ assertExactFields(
383
+ input,
384
+ PUBLIC_VERIFICATION_INPUT_FIELDS,
385
+ "contact_request_e2ee_public_verification_input_invalid",
386
+ );
387
+ const nowMs = normalizeNowOptions(
388
+ options,
389
+ "contact_request_e2ee_public_verification_options_invalid",
390
+ );
391
+ const envelope = canonicalEnvelope(input.envelope);
392
+ const senderIdentity = canonicalDeviceIdentity(input.senderDeviceIdentity);
393
+ const recipientIdentity = canonicalDeviceIdentity(input.recipientDeviceIdentity);
394
+ const recipientPreKey = canonicalPublicPreKey(input.recipientOneTimePreKey);
395
+ if (!verifyDeviceIdentityRegistration(senderIdentity)) {
396
+ throw new HelloLynkContactRequestE2eeError("contact_request_sender_identity_signature_invalid");
397
+ }
398
+ if (!verifyDeviceIdentityRegistration(recipientIdentity)) {
399
+ throw new HelloLynkContactRequestE2eeError("contact_request_recipient_identity_signature_invalid");
400
+ }
401
+ if (!verifyOneTimePreKeyRegistration({
402
+ identity: recipientIdentity,
403
+ oneTimePreKey: recipientPreKey,
404
+ })) {
405
+ throw new HelloLynkContactRequestE2eeError("contact_request_recipient_prekey_signature_invalid");
406
+ }
407
+ const expectedSender = canonicalExpectedRoute(input.expectedSender);
408
+ const expectedRecipient = canonicalExpectedRoute(input.expectedRecipient);
409
+ const protectedMetadata = envelope.protected;
410
+ if (protectedMetadata.senderIdentityKeyId !== senderIdentity.keyId) {
411
+ throw new HelloLynkContactRequestE2eeError("contact_request_sender_identity_key_mismatch");
412
+ }
413
+ if (protectedMetadata.recipientIdentityKeyId !== recipientIdentity.keyId) {
414
+ throw new HelloLynkContactRequestE2eeError("contact_request_recipient_identity_key_mismatch");
415
+ }
416
+ if (protectedMetadata.recipientPreKeyId !== recipientPreKey.preKeyId) {
417
+ throw new HelloLynkContactRequestE2eeError("contact_request_recipient_prekey_mismatch");
418
+ }
419
+ assertExpectedRoute(protectedMetadata, "sender", expectedSender);
420
+ assertExpectedRoute(protectedMetadata, "recipient", expectedRecipient);
421
+ const unsignedEnvelope = canonicalUnsignedEnvelope(envelope);
422
+ const senderPublicKey = importPublicDer(
423
+ senderIdentity.publicKey,
424
+ "ed25519",
425
+ "contact_request_sender_public_key_invalid",
426
+ );
427
+ if (!verify(
428
+ null,
429
+ domainSeparatedBytes(ENVELOPE_SIGNATURE_DOMAIN, unsignedEnvelope),
430
+ senderPublicKey,
431
+ decodeBase64Url(
432
+ envelope.signature,
433
+ 64,
434
+ "contact_request_envelope_signature_invalid",
435
+ ),
436
+ )) {
437
+ throw new HelloLynkContactRequestE2eeError("contact_request_envelope_signature_invalid");
438
+ }
439
+ assertEnvelopeTime(protectedMetadata, nowMs);
440
+ importPublicDer(
441
+ envelope.ephemeralPublicKey,
442
+ "x25519",
443
+ "contact_request_ephemeral_public_key_invalid",
444
+ );
445
+ return Object.freeze({
446
+ envelope: Object.freeze(envelope),
447
+ protectedMetadata,
448
+ envelopeDigest: digestContactRequestIntroEnvelope(envelope),
449
+ replayKey: contactRequestIntroReplayKey(protectedMetadata),
450
+ });
451
+ }
452
+
453
+ export function digestContactRequestIntroEnvelope(envelope) {
454
+ return createHash("sha256")
455
+ .update(JSON.stringify(canonicalEnvelope(envelope)), "utf8")
456
+ .digest("hex");
457
+ }
458
+
459
+ export function contactRequestIntroReplayKey(protectedMetadata) {
460
+ const accepted = canonicalProtectedMetadata(protectedMetadata);
461
+ return createHash("sha256")
462
+ .update(
463
+ `${REPLAY_KEY_DOMAIN}${accepted.senderPrincipalId}\0${accepted.attemptId}\0${accepted.clientRequestId}`,
464
+ "utf8",
465
+ )
466
+ .digest("hex");
467
+ }
468
+
469
+ export function classifyContactRequestIntroReplay(input) {
470
+ assertExactFields(
471
+ input,
472
+ ["replayKey", "envelopeDigest", "existingEnvelopeDigest"],
473
+ "contact_request_replay_input_invalid",
474
+ );
475
+ const replayKey = assertDigest(input.replayKey, "contact_request_replay_key_invalid");
476
+ const envelopeDigest = assertDigest(input.envelopeDigest, "contact_request_envelope_digest_invalid");
477
+ const existingEnvelopeDigest = input.existingEnvelopeDigest == null
478
+ ? null
479
+ : assertDigest(
480
+ input.existingEnvelopeDigest,
481
+ "contact_request_existing_envelope_digest_invalid",
482
+ );
483
+ if (existingEnvelopeDigest == null) {
484
+ return Object.freeze({ decision: "accept_new", replayKey, envelopeDigest });
485
+ }
486
+ const same = timingSafeEqual(
487
+ Buffer.from(envelopeDigest, "hex"),
488
+ Buffer.from(existingEnvelopeDigest, "hex"),
489
+ );
490
+ return Object.freeze({
491
+ decision: same ? "idempotent_replay" : "conflicting_replay",
492
+ replayKey,
493
+ envelopeDigest,
494
+ });
495
+ }
496
+
497
+ function verifyIntroCommitment(protectedMetadata, acceptedIntro, introDigestSalt) {
498
+ if (acceptedIntro.introWeight !== protectedMetadata.introWeight) {
499
+ throw new HelloLynkContactRequestE2eeError("contact_request_intro_weight_mismatch");
500
+ }
501
+ const salt = decodeBase64Url(
502
+ introDigestSalt,
503
+ INTRO_DIGEST_SALT_BYTES,
504
+ "contact_request_intro_digest_salt_invalid",
505
+ );
506
+ const expectedDigest = contactRequestIntroDigest(acceptedIntro.text, salt);
507
+ salt.fill(0);
508
+ if (!timingSafeEqual(
509
+ Buffer.from(expectedDigest, "hex"),
510
+ Buffer.from(protectedMetadata.introDigest, "hex"),
511
+ )) {
512
+ throw new HelloLynkContactRequestE2eeError("contact_request_intro_digest_mismatch");
513
+ }
514
+ }
515
+
516
+ function contactRequestIntroDigest(text, salt) {
517
+ return createHash("sha256")
518
+ .update(INTRO_DIGEST_DOMAIN)
519
+ .update(salt)
520
+ .update(Buffer.from(text, "utf8"))
521
+ .digest("hex");
522
+ }
523
+
524
+ function canonicalProtectedMetadata(input) {
525
+ assertExactFields(input, PROTECTED_FIELDS, "contact_request_protected_metadata_invalid");
526
+ const accepted = {
527
+ attemptId: assertSafeId(input.attemptId, "contact_request_attempt_id_invalid"),
528
+ clientRequestId: assertSafeId(input.clientRequestId, "contact_request_client_request_id_invalid"),
529
+ requestDigest: assertDigest(input.requestDigest, "contact_request_request_digest_invalid"),
530
+ introDigest: assertDigest(input.introDigest, "contact_request_intro_digest_invalid"),
531
+ introWeight: input.introWeight,
532
+ senderPrincipalId: assertSafeId(input.senderPrincipalId, "contact_request_sender_principal_id_invalid"),
533
+ senderHelloLynkId: assertHelloLynkId(input.senderHelloLynkId, "contact_request_sender_hellolynk_id_invalid"),
534
+ senderDeviceId: assertSafeId(input.senderDeviceId, "contact_request_sender_device_id_invalid"),
535
+ senderIdentityKeyId: assertSafeId(input.senderIdentityKeyId, "contact_request_sender_identity_key_id_invalid"),
536
+ recipientPrincipalId: assertSafeId(input.recipientPrincipalId, "contact_request_recipient_principal_id_invalid"),
537
+ recipientHelloLynkId: assertHelloLynkId(input.recipientHelloLynkId, "contact_request_recipient_hellolynk_id_invalid"),
538
+ recipientDeviceId: assertSafeId(input.recipientDeviceId, "contact_request_recipient_device_id_invalid"),
539
+ recipientIdentityKeyId: assertSafeId(input.recipientIdentityKeyId, "contact_request_recipient_identity_key_id_invalid"),
540
+ recipientPreKeyId: assertSafeId(input.recipientPreKeyId, "contact_request_recipient_prekey_id_invalid"),
541
+ authorizationScope: input.authorizationScope,
542
+ intentSource: input.intentSource,
543
+ messageType: input.messageType,
544
+ createdAt: assertIsoTimestamp(input.createdAt, "contact_request_created_at_invalid"),
545
+ expiresAt: assertIsoTimestamp(input.expiresAt, "contact_request_expires_at_invalid"),
546
+ };
547
+ if (!Number.isSafeInteger(accepted.introWeight)
548
+ || accepted.introWeight < 0
549
+ || accepted.introWeight > CONTACT_REQUEST_E2EE_MAX_WEIGHT) {
550
+ throw new HelloLynkContactRequestE2eeError("contact_request_intro_weight_invalid");
551
+ }
552
+ if (
553
+ accepted.authorizationScope !== CONTACT_REQUEST_E2EE_AUTHORIZATION_SCOPE
554
+ || accepted.intentSource !== CONTACT_REQUEST_E2EE_INTENT_SOURCE
555
+ || accepted.messageType !== CONTACT_REQUEST_E2EE_MESSAGE_TYPE
556
+ ) {
557
+ throw new HelloLynkContactRequestE2eeError("contact_request_protected_metadata_invalid");
558
+ }
559
+ if (accepted.senderPrincipalId === accepted.recipientPrincipalId) {
560
+ throw new HelloLynkContactRequestE2eeError("contact_request_self_target_invalid");
561
+ }
562
+ const expectedRequestDigest = createContactRequestRecordDigest({
563
+ recipientPrincipalId: accepted.recipientPrincipalId,
564
+ introDigest: accepted.introDigest,
565
+ introWeight: accepted.introWeight,
566
+ });
567
+ if (!timingSafeEqual(
568
+ Buffer.from(accepted.requestDigest, "hex"),
569
+ Buffer.from(expectedRequestDigest, "hex"),
570
+ )) {
571
+ throw new HelloLynkContactRequestE2eeError("contact_request_request_digest_mismatch");
572
+ }
573
+ const createdAtMs = Date.parse(accepted.createdAt);
574
+ const expiresAtMs = Date.parse(accepted.expiresAt);
575
+ if (
576
+ expiresAtMs <= createdAtMs
577
+ || expiresAtMs - createdAtMs > CONTACT_REQUEST_E2EE_MAX_TTL_MS
578
+ ) {
579
+ throw new HelloLynkContactRequestE2eeError("contact_request_expiry_window_invalid");
580
+ }
581
+ return Object.freeze(accepted);
582
+ }
583
+
584
+ function canonicalUnsignedEnvelope(input) {
585
+ const unsignedFields = ENVELOPE_FIELDS.slice(0, -1);
586
+ const source = Object.hasOwn(input, "signature")
587
+ ? {
588
+ schemaVersion: input.schemaVersion,
589
+ protocol: input.protocol,
590
+ algorithm: input.algorithm,
591
+ protected: input.protected,
592
+ ephemeralPublicKey: input.ephemeralPublicKey,
593
+ kdfSalt: input.kdfSalt,
594
+ iv: input.iv,
595
+ ciphertext: input.ciphertext,
596
+ authenticationTag: input.authenticationTag,
597
+ }
598
+ : input;
599
+ assertExactFields(source, unsignedFields, "contact_request_envelope_invalid");
600
+ if (
601
+ source.schemaVersion !== CONTACT_REQUEST_E2EE_SCHEMA_VERSION
602
+ || source.protocol !== CONTACT_REQUEST_E2EE_PROTOCOL
603
+ || source.algorithm !== CONTACT_REQUEST_E2EE_ALGORITHM
604
+ ) {
605
+ throw new HelloLynkContactRequestE2eeError("contact_request_envelope_algorithm_invalid");
606
+ }
607
+ return {
608
+ schemaVersion: source.schemaVersion,
609
+ protocol: source.protocol,
610
+ algorithm: source.algorithm,
611
+ protected: canonicalProtectedMetadata(source.protected),
612
+ ephemeralPublicKey: canonicalBase64Url(
613
+ source.ephemeralPublicKey,
614
+ 44,
615
+ "contact_request_ephemeral_public_key_invalid",
616
+ ),
617
+ kdfSalt: canonicalBase64Url(source.kdfSalt, 32, "contact_request_kdf_salt_invalid"),
618
+ iv: canonicalBase64Url(source.iv, 12, "contact_request_iv_invalid"),
619
+ ciphertext: canonicalBase64Url(
620
+ source.ciphertext,
621
+ null,
622
+ "contact_request_ciphertext_invalid",
623
+ { minimumBytes: INTRO_DIGEST_SALT_BYTES, maximumBytes: MAX_CIPHERTEXT_BYTES },
624
+ ),
625
+ authenticationTag: canonicalBase64Url(
626
+ source.authenticationTag,
627
+ 16,
628
+ "contact_request_authentication_tag_invalid",
629
+ ),
630
+ };
631
+ }
632
+
633
+ function canonicalEnvelope(input) {
634
+ assertExactFields(input, ENVELOPE_FIELDS, "contact_request_envelope_invalid");
635
+ const unsigned = canonicalUnsignedEnvelope(input);
636
+ return {
637
+ ...unsigned,
638
+ signature: canonicalBase64Url(
639
+ input.signature,
640
+ 64,
641
+ "contact_request_envelope_signature_invalid",
642
+ ),
643
+ };
644
+ }
645
+
646
+ function canonicalExpectedRoute(input) {
647
+ assertExactFields(input, EXPECTED_ROUTE_FIELDS, "contact_request_expected_route_invalid");
648
+ return {
649
+ principalId: assertSafeId(input.principalId, "contact_request_expected_route_invalid"),
650
+ hellolynkId: assertHelloLynkId(input.hellolynkId, "contact_request_expected_route_invalid"),
651
+ deviceId: assertSafeId(input.deviceId, "contact_request_expected_route_invalid"),
652
+ };
653
+ }
654
+
655
+ function assertExpectedRoute(protectedMetadata, prefix, expected) {
656
+ if (
657
+ protectedMetadata[`${prefix}PrincipalId`] !== expected.principalId
658
+ || protectedMetadata[`${prefix}HelloLynkId`] !== expected.hellolynkId
659
+ || protectedMetadata[`${prefix}DeviceId`] !== expected.deviceId
660
+ ) {
661
+ throw new HelloLynkContactRequestE2eeError(`contact_request_${prefix}_route_mismatch`);
662
+ }
663
+ }
664
+
665
+ function canonicalDeviceIdentity(input) {
666
+ assertExactFields(
667
+ input,
668
+ ["schemaVersion", "keyId", "algorithm", "publicKey", "createdAt", "ownershipSignature"],
669
+ "contact_request_device_identity_invalid",
670
+ );
671
+ if (input.schemaVersion !== DIRECT_TEXT_E2EE_KEY_SCHEMA_VERSION || input.algorithm !== "Ed25519") {
672
+ throw new HelloLynkContactRequestE2eeError("contact_request_device_identity_invalid");
673
+ }
674
+ canonicalBase64Url(input.publicKey, 44, "contact_request_device_public_key_invalid");
675
+ canonicalBase64Url(input.ownershipSignature, 64, "contact_request_identity_signature_invalid");
676
+ return Object.freeze({
677
+ schemaVersion: input.schemaVersion,
678
+ keyId: assertSafeId(input.keyId, "contact_request_identity_key_id_invalid"),
679
+ algorithm: input.algorithm,
680
+ publicKey: input.publicKey,
681
+ createdAt: assertIsoTimestamp(input.createdAt, "contact_request_identity_created_at_invalid"),
682
+ ownershipSignature: input.ownershipSignature,
683
+ });
684
+ }
685
+
686
+ function canonicalPublicPreKey(input) {
687
+ assertExactFields(
688
+ input,
689
+ ["schemaVersion", "preKeyId", "identityKeyId", "algorithm", "publicKey", "createdAt", "signature"],
690
+ "contact_request_public_prekey_invalid",
691
+ );
692
+ if (input.schemaVersion !== DIRECT_TEXT_E2EE_KEY_SCHEMA_VERSION || input.algorithm !== "X25519") {
693
+ throw new HelloLynkContactRequestE2eeError("contact_request_public_prekey_invalid");
694
+ }
695
+ canonicalBase64Url(input.publicKey, 44, "contact_request_prekey_public_invalid");
696
+ canonicalBase64Url(input.signature, 64, "contact_request_prekey_signature_invalid");
697
+ return Object.freeze({
698
+ schemaVersion: input.schemaVersion,
699
+ preKeyId: assertSafeId(input.preKeyId, "contact_request_prekey_id_invalid"),
700
+ identityKeyId: assertSafeId(input.identityKeyId, "contact_request_identity_key_id_invalid"),
701
+ algorithm: input.algorithm,
702
+ publicKey: input.publicKey,
703
+ createdAt: assertIsoTimestamp(input.createdAt, "contact_request_prekey_created_at_invalid"),
704
+ signature: input.signature,
705
+ });
706
+ }
707
+
708
+ function publicIdentityFromPrivateBundle(bundle) {
709
+ try {
710
+ return publicDirectTextDeviceIdentity(bundle);
711
+ } catch {
712
+ throw new HelloLynkContactRequestE2eeError("contact_request_device_key_bundle_invalid");
713
+ }
714
+ }
715
+
716
+ function privateIdentityKey(bundle) {
717
+ publicIdentityFromPrivateBundle(bundle);
718
+ return importPrivateDer(
719
+ bundle.privateKey,
720
+ "ed25519",
721
+ "contact_request_sender_private_key_invalid",
722
+ );
723
+ }
724
+
725
+ function privatePreKeyFromBundle(preKeyBundle, deviceKeyBundle) {
726
+ let publicPreKey;
727
+ try {
728
+ publicPreKey = publicOneTimePreKey(preKeyBundle, deviceKeyBundle);
729
+ } catch {
730
+ throw new HelloLynkContactRequestE2eeError("contact_request_private_prekey_invalid");
731
+ }
732
+ return {
733
+ preKeyId: publicPreKey.preKeyId,
734
+ identityKeyId: publicPreKey.identityKeyId,
735
+ privateKey: preKeyBundle.privateKey,
736
+ publicPreKey,
737
+ };
738
+ }
739
+
740
+ function assertEnvelopeTime(protectedMetadata, nowMs) {
741
+ const createdAtMs = Date.parse(protectedMetadata.createdAt);
742
+ const expiresAtMs = Date.parse(protectedMetadata.expiresAt);
743
+ if (createdAtMs > nowMs + MAX_CLOCK_SKEW_MS) {
744
+ throw new HelloLynkContactRequestE2eeError("contact_request_created_at_in_future");
745
+ }
746
+ if (expiresAtMs <= nowMs) {
747
+ throw new HelloLynkContactRequestE2eeError("contact_request_envelope_expired");
748
+ }
749
+ }
750
+
751
+ function normalizeNowOptions(options, code) {
752
+ assertPlainRecord(options, code);
753
+ assertOnlyKnownFields(options, ["nowMs"], code);
754
+ const nowMs = options.nowMs == null ? Date.now() : Number(options.nowMs);
755
+ if (!Number.isSafeInteger(nowMs) || nowMs <= 0) {
756
+ throw new HelloLynkContactRequestE2eeError(code);
757
+ }
758
+ return nowMs;
759
+ }
760
+
761
+ function assertIsoTimestamp(value, code) {
762
+ if (typeof value !== "string") throw new HelloLynkContactRequestE2eeError(code);
763
+ const parsed = Date.parse(value);
764
+ if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) {
765
+ throw new HelloLynkContactRequestE2eeError(code);
766
+ }
767
+ return value;
768
+ }
769
+
770
+ function assertSafeId(value, code) {
771
+ if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,199}$/.test(value)) {
772
+ throw new HelloLynkContactRequestE2eeError(code);
773
+ }
774
+ return value;
775
+ }
776
+
777
+ function assertHelloLynkId(value, code) {
778
+ if (typeof value !== "string" || !/^hellolynk:(intl|cn):[a-z0-9][a-z0-9._-]{2,179}$/.test(value)) {
779
+ throw new HelloLynkContactRequestE2eeError(code);
780
+ }
781
+ return value;
782
+ }
783
+
784
+ function assertDigest(value, code) {
785
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) {
786
+ throw new HelloLynkContactRequestE2eeError(code);
787
+ }
788
+ return value;
789
+ }
790
+
791
+ function assertPlainRecord(value, code) {
792
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
793
+ throw new HelloLynkContactRequestE2eeError(code);
794
+ }
795
+ const prototype = Object.getPrototypeOf(value);
796
+ if (prototype !== Object.prototype && prototype !== null) {
797
+ throw new HelloLynkContactRequestE2eeError(code);
798
+ }
799
+ }
800
+
801
+ function assertOnlyKnownFields(value, expectedFields, code) {
802
+ if (Object.keys(value).some((field) => !expectedFields.includes(field))) {
803
+ throw new HelloLynkContactRequestE2eeError(code);
804
+ }
805
+ }
806
+
807
+ function assertExactFields(value, expectedFields, code) {
808
+ assertPlainRecord(value, code);
809
+ const actualFields = Object.keys(value);
810
+ if (
811
+ actualFields.length !== expectedFields.length
812
+ || expectedFields.some((field) => !Object.hasOwn(value, field))
813
+ ) {
814
+ throw new HelloLynkContactRequestE2eeError(code);
815
+ }
816
+ }
817
+
818
+ function canonicalBase64Url(value, expectedBytes, code, limits = {}) {
819
+ return decodeBase64Url(value, expectedBytes, code, limits).toString("base64url");
820
+ }
821
+
822
+ function decodeBase64Url(value, expectedBytes, code, limits = {}) {
823
+ if (typeof value !== "string"
824
+ || (!/^[A-Za-z0-9_-]+$/.test(value) && !(value === "" && limits.minimumBytes === 0))) {
825
+ throw new HelloLynkContactRequestE2eeError(code);
826
+ }
827
+ const decoded = Buffer.from(value, "base64url");
828
+ if (decoded.toString("base64url") !== value) throw new HelloLynkContactRequestE2eeError(code);
829
+ if (expectedBytes != null && decoded.length !== expectedBytes) {
830
+ throw new HelloLynkContactRequestE2eeError(code);
831
+ }
832
+ if (limits.minimumBytes != null && decoded.length < limits.minimumBytes) {
833
+ throw new HelloLynkContactRequestE2eeError(code);
834
+ }
835
+ if (limits.maximumBytes != null && decoded.length > limits.maximumBytes) {
836
+ throw new HelloLynkContactRequestE2eeError(code);
837
+ }
838
+ return decoded;
839
+ }
840
+
841
+ function importPrivateDer(value, asymmetricKeyType, code) {
842
+ try {
843
+ const der = decodeBase64Url(value, 48, code);
844
+ const key = createPrivateKey({ key: der, format: "der", type: "pkcs8" });
845
+ if (key.asymmetricKeyType !== asymmetricKeyType) throw new Error("type");
846
+ return key;
847
+ } catch (error) {
848
+ if (error instanceof HelloLynkContactRequestE2eeError) throw error;
849
+ throw new HelloLynkContactRequestE2eeError(code);
850
+ }
851
+ }
852
+
853
+ function importPublicDer(value, asymmetricKeyType, code) {
854
+ try {
855
+ const der = decodeBase64Url(value, 44, code);
856
+ const key = createPublicKey({ key: der, format: "der", type: "spki" });
857
+ if (key.asymmetricKeyType !== asymmetricKeyType) throw new Error("type");
858
+ return key;
859
+ } catch (error) {
860
+ if (error instanceof HelloLynkContactRequestE2eeError) throw error;
861
+ throw new HelloLynkContactRequestE2eeError(code);
862
+ }
863
+ }
864
+
865
+ function domainSeparatedBytes(domain, value) {
866
+ return Buffer.from(`${domain}${JSON.stringify(value)}`, "utf8");
867
+ }