@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,1228 @@
1
+ import crypto from "node:crypto";
2
+ import { lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { defaultHelloLynkClientRoot } from "./hellolynk-client-profile.mjs";
5
+ import { assertLocalDataProtectionAccess } from "./hellolynk-local-data-protection.mjs";
6
+ import runtimeSafety from "./hellolynk-runtime-safety.cjs";
7
+
8
+ const {
9
+ HELLOLYNK_CONTACTS_CONTRACT: CONTACTS_CONTRACT,
10
+ classifyHelloLynkStableId,
11
+ isHelloLynkStableIdReservedText,
12
+ } = runtimeSafety;
13
+ const SCHEMA_VERSION = 4;
14
+ const READABLE_SCHEMA_VERSIONS = new Set([2, 3, SCHEMA_VERSION]);
15
+ export const HELLOLYNK_LOCAL_CONTACTS_CONTRACT = CONTACTS_CONTRACT;
16
+ export const HELLOLYNK_LOCAL_CONTACTS_SCHEMA_VERSION = SCHEMA_VERSION;
17
+ const MAX_CONTACTS = 500;
18
+ const MAX_RELATIONSHIP_SYNC_RECORDS = 2000;
19
+ const MAX_AUTHORIZED_DELEGATES_PER_CONTACT = 32;
20
+ const MAX_INSTANCES_PER_CONTACT = 32;
21
+ const INSTANCE_CLIENT_TYPES = new Set(["codex", "openclaw", "trae-work", "workbuddy", "cursor", "dsh", "claude-code"]);
22
+ const MAX_STORE_BYTES = 1024 * 1024;
23
+ const MAX_PROTECTED_STORE_BYTES = MAX_STORE_BYTES * 2;
24
+ const LOCK_ATTEMPTS = 100;
25
+ const LOCK_WAIT_MS = 20;
26
+ const LOCK_STALE_MS = 5_000;
27
+ const PROFILE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{2,127}$/;
28
+ const CONTACT_ID_RE = /^contact_[a-f0-9-]{36}$/;
29
+ const DELEGATE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,198}$/;
30
+ const SAFETY_CODE_RE = /^[A-F0-9]{4}(?:-[A-F0-9]{4}){11}$/;
31
+ const SAFETY_CODE_VERIFICATION_STATES = Object.freeze({
32
+ unavailable: "unavailable",
33
+ verificationRequired: "verification-required",
34
+ locallyVerified: "locally-verified",
35
+ });
36
+ const UNSAFE_DISPLAY_RE = /[\u0000-\u001F\u007F-\u009F\u061C\u200E\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069]/u;
37
+ const mutationQueues = new Map();
38
+ const NO_STORE_WRITE = Symbol("hellolynk-local-contacts-no-store-write");
39
+
40
+ export class HelloLynkLocalContactsError extends Error {
41
+ constructor(code, options = {}) {
42
+ super(code, options.cause ? { cause: options.cause } : undefined);
43
+ this.name = "HelloLynkLocalContactsError";
44
+ this.code = code;
45
+ }
46
+ }
47
+
48
+ export function normalizeContactHelloLynkId(value, options = {}) {
49
+ const hellolynkId = String(value || "").trim();
50
+ const identity = classifyHelloLynkStableId(hellolynkId);
51
+ if (!identity) {
52
+ throw new HelloLynkLocalContactsError("contact_hellolynk_id_invalid");
53
+ }
54
+ return hellolynkId;
55
+ }
56
+
57
+ export function normalizeContactDisplayName(value) {
58
+ const displayName = String(value || "").normalize("NFKC").trim().replace(/\s+/gu, " ");
59
+ if (
60
+ !displayName ||
61
+ [...displayName].length > 80 ||
62
+ UNSAFE_DISPLAY_RE.test(displayName) ||
63
+ isHelloLynkStableIdReservedText(displayName)
64
+ ) {
65
+ throw new HelloLynkLocalContactsError("contact_display_name_invalid");
66
+ }
67
+ return displayName;
68
+ }
69
+
70
+ export function contactDisplayNameKey(value) {
71
+ return normalizeContactDisplayName(value).toLocaleLowerCase("und");
72
+ }
73
+
74
+ function normalizeContactSafetyCode(value) {
75
+ if (value == null || String(value).trim() === "") return null;
76
+ const safetyCode = String(value).trim().toUpperCase().replace(/[\s:]+/gu, "-");
77
+ if (!SAFETY_CODE_RE.test(safetyCode)) throw new HelloLynkLocalContactsError("contact_safety_code_invalid");
78
+ return safetyCode;
79
+ }
80
+
81
+ export function resolveLocalContactsPath(options = {}) {
82
+ const profileId = normalizeProfileId(options.profileId);
83
+ const root = path.resolve(options.root || defaultHelloLynkClientRoot());
84
+ return path.join(root, "contacts", `${profileId}.json`);
85
+ }
86
+
87
+ export function createLocalContactStore(options = {}) {
88
+ const profileId = normalizeProfileId(options.profileId);
89
+ const storePath = path.resolve(options.storePath || resolveLocalContactsPath({
90
+ profileId,
91
+ root: options.root,
92
+ }));
93
+ const now = typeof options.now === "function" ? options.now : () => new Date();
94
+ const protection = normalizeContactDataProtection(options.dataProtector, profileId);
95
+
96
+ async function snapshot() {
97
+ const store = await readStore(storePath, profileId, protection);
98
+ return publicSnapshot(store, storePath);
99
+ }
100
+
101
+ async function save(input = {}) {
102
+ const hellolynkId = normalizeContactHelloLynkId(input.hellolynkId);
103
+ const stableIdentity = classifyHelloLynkStableId(hellolynkId);
104
+ const displayName = normalizeContactDisplayName(input.displayName);
105
+ if (Object.hasOwn(input, "safetyCode")) throw new HelloLynkLocalContactsError("contact_safety_code_invalid");
106
+ const requestedContactId = input.contactId == null ? null : normalizeContactId(input.contactId);
107
+ return mutateStore(storePath, profileId, async (store) => {
108
+ const displayKey = contactDisplayNameKey(displayName);
109
+ const byRequestedId = requestedContactId
110
+ ? store.contacts.find((contact) => contact.contactId === requestedContactId)
111
+ : null;
112
+ if (requestedContactId && !byRequestedId) throw new HelloLynkLocalContactsError("contact_not_found");
113
+ const byHelloLynkId = store.contacts.find((contact) => contact.hellolynkId === hellolynkId);
114
+ const byStableAlias = store.contacts.find((contact) => (
115
+ classifyHelloLynkStableId(contact.hellolynkId)?.aliasKey === stableIdentity.aliasKey
116
+ ));
117
+ if (byStableAlias && byStableAlias.hellolynkId !== hellolynkId) {
118
+ throw new HelloLynkLocalContactsError("contact_hellolynk_id_alias_conflict");
119
+ }
120
+ if (byHelloLynkId && byRequestedId && byHelloLynkId.contactId !== byRequestedId.contactId) {
121
+ throw new HelloLynkLocalContactsError("contact_hellolynk_id_conflict");
122
+ }
123
+ const target = byRequestedId || byHelloLynkId || null;
124
+ const conflictingName = store.contacts.find((contact) => (
125
+ contactDisplayNameKey(contact.displayName) === displayKey && contact.contactId !== target?.contactId
126
+ ));
127
+ if (conflictingName) throw new HelloLynkLocalContactsError("contact_display_name_conflict");
128
+ const timestamp = normalizeTimestamp(now());
129
+ if (target) {
130
+ const hellolynkIdChanged = target.hellolynkId !== hellolynkId;
131
+ if (hellolynkIdChanged) {
132
+ rememberDeletedContact(store, target.hellolynkId);
133
+ target.autonomousSendDelegateIds = [];
134
+ target.continuousReadDelegateIds = [];
135
+ target.principalId = null;
136
+ target.instances = [];
137
+ }
138
+ target.hellolynkId = hellolynkId;
139
+ target.displayName = displayName;
140
+ target.updatedAt = timestamp;
141
+ clearPendingContactDeletion(store, hellolynkId);
142
+ return publicContact(target);
143
+ }
144
+ if (store.contacts.length >= MAX_CONTACTS) throw new HelloLynkLocalContactsError("contact_capacity_exceeded");
145
+ const contact = {
146
+ contactId: `contact_${crypto.randomUUID()}`,
147
+ hellolynkId,
148
+ displayName,
149
+ principalId: null,
150
+ instances: [],
151
+ safetyCode: null,
152
+ safetyCodeVerificationState: SAFETY_CODE_VERIFICATION_STATES.unavailable,
153
+ autonomousSendDelegateIds: [],
154
+ continuousReadDelegateIds: [],
155
+ createdAt: timestamp,
156
+ updatedAt: timestamp,
157
+ };
158
+ store.contacts.push(contact);
159
+ clearPendingContactDeletion(store, hellolynkId);
160
+ return publicContact(contact);
161
+ }, protection);
162
+ }
163
+
164
+ async function rename(contactId, input = {}) {
165
+ const acceptedContactId = normalizeContactId(contactId);
166
+ assertExactObjectFields(input, ["displayName", "expectedDisplayName", "expectedHelloLynkId"]);
167
+ const displayName = normalizeContactDisplayName(input.displayName);
168
+ const expectedDisplayName = normalizeContactDisplayName(input.expectedDisplayName);
169
+ const expectedHelloLynkId = normalizeContactHelloLynkId(input.expectedHelloLynkId);
170
+ return mutateStore(storePath, profileId, async (store) => {
171
+ const contact = store.contacts.find((item) => item.contactId === acceptedContactId);
172
+ if (!contact) throw new HelloLynkLocalContactsError("contact_not_found");
173
+ // Compare only the identity and note being edited, under the same write
174
+ // lock. New messages or permission updates are not competing note edits.
175
+ if (contact.hellolynkId !== expectedHelloLynkId || contact.displayName !== expectedDisplayName) {
176
+ throw new HelloLynkLocalContactsError("contact_name_conflict");
177
+ }
178
+ if (store.contacts.some((item) => item.contactId !== acceptedContactId
179
+ && contactDisplayNameKey(item.displayName) === contactDisplayNameKey(displayName))) {
180
+ throw new HelloLynkLocalContactsError("contact_display_name_conflict");
181
+ }
182
+ if (contact.displayName === displayName) return noStoreWrite(publicContact(contact));
183
+ contact.displayName = displayName;
184
+ contact.updatedAt = normalizeTimestamp(now());
185
+ return publicContact(contact);
186
+ }, protection);
187
+ }
188
+
189
+ async function remove(contactId) {
190
+ const acceptedId = normalizeContactId(contactId);
191
+ return mutateStore(storePath, profileId, async (store) => {
192
+ const index = store.contacts.findIndex((contact) => contact.contactId === acceptedId);
193
+ if (index < 0) return noStoreWrite(false);
194
+ rememberDeletedContact(store, store.contacts[index].hellolynkId);
195
+ store.contacts.splice(index, 1);
196
+ return true;
197
+ }, protection);
198
+ }
199
+
200
+ async function synchronizeRelationships(input = {}) {
201
+ const { owner, relationships } = normalizeRelationshipSynchronization(input);
202
+ return mutateStore(storePath, profileId, async (store) => {
203
+ const sync = store.relationshipSync;
204
+ if (sync.owner && (sync.owner.principalId !== owner.principalId
205
+ || sync.owner.hellolynkId !== owner.hellolynkId)) {
206
+ throw new HelloLynkLocalContactsError("contact_relationship_owner_mismatch");
207
+ }
208
+ if (sync.owner == null) {
209
+ const identity = classifyHelloLynkStableId(owner.hellolynkId);
210
+ sync.pendingDeletedHelloLynkIds = sync.pendingDeletedHelloLynkIds.filter((peerId) => {
211
+ const peer = classifyHelloLynkStableId(peerId);
212
+ return peer.region === identity.region && peer.aliasKey !== identity.aliasKey;
213
+ });
214
+ }
215
+ for (const relationship of relationships) {
216
+ const known = sync.generations.find((item) => item.relationshipId === relationship.relationshipId);
217
+ if (known && known.hellolynkId !== relationship.otherPrincipal.hellolynkId) {
218
+ throw new HelloLynkLocalContactsError("contact_relationship_identity_conflict");
219
+ }
220
+ }
221
+ let changed = sync.owner == null;
222
+ sync.owner = owner;
223
+ let addedCount = 0;
224
+ let capacityReached = false;
225
+ if (serializedStoreBytes(store) > MAX_STORE_BYTES) {
226
+ return noStoreWrite({ addedCount: 0, capacityReached: true });
227
+ }
228
+ for (const relationship of relationships) {
229
+ const peer = relationship.otherPrincipal.hellolynkId;
230
+ const known = sync.generations.find((item) => item.relationshipId === relationship.relationshipId);
231
+ const pending = sync.pendingDeletedHelloLynkIds.includes(peer);
232
+ const contactExists = store.contacts.some((contact) => contact.hellolynkId === peer);
233
+ const suppressed = known?.suppressed === true || pending || relationship.status === "ended";
234
+ const addContact = relationship.status === "active" && !suppressed && !contactExists;
235
+ if ((addContact && store.contacts.length >= MAX_CONTACTS)
236
+ || (!known && sync.generations.length + sync.pendingDeletedHelloLynkIds.length
237
+ - Number(pending) >= MAX_RELATIONSHIP_SYNC_RECORDS)) {
238
+ capacityReached = true;
239
+ continue;
240
+ }
241
+ const previousGenerations = sync.generations;
242
+ const previousPending = sync.pendingDeletedHelloLynkIds;
243
+ let relationshipChanged = false;
244
+ if (!known || known.suppressed !== suppressed) {
245
+ sync.generations = sync.generations.filter((item) => item !== known).concat({
246
+ relationshipId: relationship.relationshipId, hellolynkId: peer, suppressed,
247
+ });
248
+ relationshipChanged = true;
249
+ }
250
+ if (pending) {
251
+ clearPendingContactDeletion(store, peer);
252
+ relationshipChanged = true;
253
+ }
254
+ if (addContact) {
255
+ const timestamp = normalizeTimestamp(now());
256
+ store.contacts.push({
257
+ contactId: `contact_${crypto.randomUUID()}`,
258
+ hellolynkId: peer,
259
+ displayName: automaticContactDisplayName(relationship.otherPrincipal.displayName, peer, store.contacts),
260
+ principalId: null,
261
+ instances: [],
262
+ safetyCode: null,
263
+ safetyCodeVerificationState: SAFETY_CODE_VERIFICATION_STATES.unavailable,
264
+ autonomousSendDelegateIds: [],
265
+ continuousReadDelegateIds: [],
266
+ createdAt: timestamp,
267
+ updatedAt: timestamp,
268
+ });
269
+ relationshipChanged = true;
270
+ }
271
+ if (!relationshipChanged) continue;
272
+ if (serializedStoreBytes(store) > MAX_STORE_BYTES) {
273
+ sync.generations = previousGenerations;
274
+ sync.pendingDeletedHelloLynkIds = previousPending;
275
+ if (addContact) store.contacts.pop();
276
+ capacityReached = true;
277
+ continue;
278
+ }
279
+ changed = true;
280
+ if (addContact) addedCount += 1;
281
+ }
282
+ const result = { addedCount, capacityReached };
283
+ return changed ? result : noStoreWrite(result);
284
+ }, protection);
285
+ }
286
+
287
+ // Only the Host may supply the account-bound instance directory result.
288
+ // The stable delegate ID controls identity and routing; displayName is this
289
+ // local contact's label for that instance.
290
+ async function upsertInstance(contactId, input = {}) {
291
+ const acceptedContactId = normalizeContactId(contactId);
292
+ assertExactObjectFields(input, ["principalId", "delegateId", "clientType", "displayName"]);
293
+ const principalId = normalizeDelegateId(input.principalId);
294
+ const delegateId = normalizeDelegateId(input.delegateId);
295
+ const clientType = normalizeInstanceClientType(input.clientType);
296
+ const displayName = normalizeContactDisplayName(input.displayName);
297
+ return mutateStore(storePath, profileId, async (store) => {
298
+ const contact = store.contacts.find((item) => item.contactId === acceptedContactId);
299
+ if (!contact) throw new HelloLynkLocalContactsError("contact_not_found");
300
+ if (contact.principalId != null && contact.principalId !== principalId) {
301
+ throw new HelloLynkLocalContactsError("contact_instance_principal_conflict");
302
+ }
303
+ const timestamp = normalizeTimestamp(now());
304
+ const instance = contact.instances.find((item) => item.delegateId === delegateId);
305
+ if (instance) {
306
+ if (instance.clientType !== clientType) throw new HelloLynkLocalContactsError("contact_instance_client_conflict");
307
+ instance.displayName = displayName;
308
+ instance.updatedAt = timestamp;
309
+ } else {
310
+ if (contact.instances.length >= MAX_INSTANCES_PER_CONTACT) {
311
+ throw new HelloLynkLocalContactsError("contact_instance_capacity_exceeded");
312
+ }
313
+ contact.instances.push({ delegateId, clientType, displayName,
314
+ autonomousSendDelegateIds: [], continuousReadDelegateIds: [], createdAt: timestamp, updatedAt: timestamp });
315
+ }
316
+ contact.principalId = principalId;
317
+ contact.updatedAt = timestamp;
318
+ return publicContact(contact);
319
+ }, protection);
320
+ }
321
+
322
+ async function renameInstance(contactId, input = {}) {
323
+ const acceptedContactId = normalizeContactId(contactId);
324
+ assertExactObjectFields(input, ["delegateId", "displayName", "expectedDisplayName", "expectedHelloLynkId", "expectedPrincipalId"]);
325
+ const delegateId = normalizeDelegateId(input.delegateId);
326
+ const displayName = normalizeContactDisplayName(input.displayName);
327
+ const expectedDisplayName = normalizeContactDisplayName(input.expectedDisplayName);
328
+ const expectedHelloLynkId = normalizeContactHelloLynkId(input.expectedHelloLynkId);
329
+ const expectedPrincipalId = normalizeDelegateId(input.expectedPrincipalId);
330
+ return mutateStore(storePath, profileId, async (store) => {
331
+ const contact = store.contacts.find((item) => item.contactId === acceptedContactId);
332
+ if (!contact) throw new HelloLynkLocalContactsError("contact_not_found");
333
+ if (contact.hellolynkId !== expectedHelloLynkId || contact.principalId !== expectedPrincipalId) {
334
+ throw new HelloLynkLocalContactsError("contact_identity_mismatch");
335
+ }
336
+ // Require the same already saved instance under the write lock. A stale
337
+ // editor must never recreate a deleted entry or overwrite a newer note.
338
+ const instance = requireContactInstance(contact, delegateId);
339
+ if (instance.displayName !== expectedDisplayName) {
340
+ throw new HelloLynkLocalContactsError("contact_instance_name_conflict");
341
+ }
342
+ const timestamp = normalizeTimestamp(now());
343
+ instance.displayName = displayName;
344
+ instance.updatedAt = timestamp;
345
+ contact.updatedAt = timestamp;
346
+ return publicContact(contact);
347
+ }, protection);
348
+ }
349
+
350
+ async function removeInstance(contactId, peerDelegateId) {
351
+ const acceptedContactId = normalizeContactId(contactId);
352
+ const acceptedPeerDelegateId = normalizeDelegateId(peerDelegateId);
353
+ return mutateStore(storePath, profileId, async (store) => {
354
+ const contact = store.contacts.find((item) => item.contactId === acceptedContactId);
355
+ if (!contact) throw new HelloLynkLocalContactsError("contact_not_found");
356
+ const index = contact.instances.findIndex((item) => item.delegateId === acceptedPeerDelegateId);
357
+ if (index < 0) return noStoreWrite(false);
358
+ contact.instances.splice(index, 1);
359
+ contact.updatedAt = normalizeTimestamp(now());
360
+ return true;
361
+ }, protection);
362
+ }
363
+
364
+ async function resolveInstance(contactId, peerDelegateId) {
365
+ const acceptedContactId = normalizeContactId(contactId);
366
+ const acceptedPeerDelegateId = normalizeDelegateId(peerDelegateId);
367
+ const store = await readStore(storePath, profileId, protection);
368
+ const contact = store.contacts.find((item) => item.contactId === acceptedContactId);
369
+ if (!contact) throw new HelloLynkLocalContactsError("contact_not_found");
370
+ const instance = requireContactInstance(contact, acceptedPeerDelegateId);
371
+ return resolvedInstance(contact, instance);
372
+ }
373
+
374
+ async function resolve(value) {
375
+ const raw = String(value || "").trim();
376
+ if (!raw || raw.length > 200 || /[\r\n\u0000-\u001F\u007F]/u.test(raw)) {
377
+ throw new HelloLynkLocalContactsError("contact_reference_invalid");
378
+ }
379
+ const directIdentity = classifyHelloLynkStableId(raw);
380
+ if (directIdentity) return {
381
+ hellolynkId: raw,
382
+ displayName: raw,
383
+ source: "hellolynk_id",
384
+ };
385
+ const wanted = contactDisplayNameKey(raw);
386
+ const store = await readStore(storePath, profileId, protection);
387
+ const matches = store.contacts.filter((contact) => contactDisplayNameKey(contact.displayName) === wanted);
388
+ if (matches.length !== 1) {
389
+ throw new HelloLynkLocalContactsError(matches.length ? "contact_reference_ambiguous" : "contact_not_found");
390
+ }
391
+ return {
392
+ hellolynkId: matches[0].hellolynkId,
393
+ displayName: matches[0].displayName,
394
+ contactId: matches[0].contactId,
395
+ source: "local_contact",
396
+ };
397
+ }
398
+
399
+ async function setAutonomousSendAuthorization(contactId, delegateId, enabled, peerDelegateId = null) {
400
+ const acceptedContactId = normalizeContactId(contactId);
401
+ const acceptedDelegateId = normalizeDelegateId(delegateId);
402
+ const acceptedPeerDelegateId = peerDelegateId == null ? null : normalizeDelegateId(peerDelegateId);
403
+ if (typeof enabled !== "boolean") throw new HelloLynkLocalContactsError("contact_authorization_enabled_invalid");
404
+ return mutateStore(storePath, profileId, async (store) => {
405
+ const contact = store.contacts.find((item) => item.contactId === acceptedContactId);
406
+ if (!contact) throw new HelloLynkLocalContactsError("contact_not_found");
407
+ const delegates = new Set(contact.autonomousSendDelegateIds);
408
+ const instance = acceptedPeerDelegateId == null ? null : requireContactInstance(contact, acceptedPeerDelegateId);
409
+ if (enabled) {
410
+ if (!delegates.has(acceptedDelegateId)
411
+ && delegates.size >= MAX_AUTHORIZED_DELEGATES_PER_CONTACT) {
412
+ throw new HelloLynkLocalContactsError("contact_authorization_capacity_exceeded");
413
+ }
414
+ delegates.add(acceptedDelegateId);
415
+ } else if (!instance) {
416
+ delegates.delete(acceptedDelegateId);
417
+ }
418
+ if (instance) {
419
+ const instanceDelegates = new Set(instance.autonomousSendDelegateIds);
420
+ if (enabled) {
421
+ if (!instanceDelegates.has(acceptedDelegateId)
422
+ && instanceDelegates.size >= MAX_AUTHORIZED_DELEGATES_PER_CONTACT) {
423
+ throw new HelloLynkLocalContactsError("contact_authorization_capacity_exceeded");
424
+ }
425
+ instanceDelegates.add(acceptedDelegateId);
426
+ } else instanceDelegates.delete(acceptedDelegateId);
427
+ instance.autonomousSendDelegateIds = [...instanceDelegates].sort();
428
+ instance.updatedAt = normalizeTimestamp(now());
429
+ } else if (!enabled) {
430
+ for (const child of contact.instances) {
431
+ child.autonomousSendDelegateIds = child.autonomousSendDelegateIds.filter((id) => id !== acceptedDelegateId);
432
+ }
433
+ }
434
+ contact.autonomousSendDelegateIds = [...delegates].sort();
435
+ contact.updatedAt = normalizeTimestamp(now());
436
+ return projectContactForDelegate(contact, acceptedDelegateId);
437
+ }, protection);
438
+ }
439
+
440
+ async function getAiReadAuthorization(localDelegateId) {
441
+ normalizeDelegateId(localDelegateId);
442
+ // Reading the owner's data is intrinsic; persisted legacy read grants do
443
+ // not constrain it. Send grants remain separate and revocable.
444
+ return Object.freeze({ contactRequests: true });
445
+ }
446
+
447
+ async function listForDelegate(delegateId) {
448
+ const acceptedDelegateId = normalizeDelegateId(delegateId);
449
+ const store = await readStore(storePath, profileId, protection);
450
+ return store.contacts
451
+ .slice()
452
+ .sort((left, right) => left.displayName.localeCompare(right.displayName, "zh-CN"))
453
+ .map((contact) => projectContactForDelegate(contact, acceptedDelegateId));
454
+ }
455
+
456
+ async function listAuthorizedForDelegate(delegateId) {
457
+ return listAuthorizedRoutesForDelegate(delegateId);
458
+ }
459
+
460
+ async function listAuthorizedRoutesForDelegate(delegateId) {
461
+ const acceptedDelegateId = normalizeDelegateId(delegateId);
462
+ const store = await readStore(storePath, profileId, protection);
463
+ return store.contacts
464
+ .filter((contact) => contact.autonomousSendDelegateIds.includes(acceptedDelegateId))
465
+ .flatMap((contact) => contact.instances
466
+ .filter((instance) => instance.autonomousSendDelegateIds.includes(acceptedDelegateId))
467
+ .map((instance) => ({
468
+ contactId: contact.contactId,
469
+ hellolynkId: contact.hellolynkId,
470
+ principalId: contact.principalId,
471
+ displayName: contact.displayName,
472
+ recipientDelegateId: instance.delegateId,
473
+ instanceDisplayName: instance.displayName,
474
+ clientType: instance.clientType,
475
+ })));
476
+ }
477
+
478
+ async function listReadableRoutesForDelegate(localDelegateId) {
479
+ normalizeDelegateId(localDelegateId);
480
+ const store = await readStore(storePath, profileId, protection);
481
+ return store.contacts
482
+ .flatMap((contact) => contact.instances
483
+ .map((instance) => ({
484
+ contactId: contact.contactId,
485
+ hellolynkId: contact.hellolynkId,
486
+ principalId: contact.principalId,
487
+ displayName: contact.displayName,
488
+ recipientDelegateId: instance.delegateId,
489
+ instanceDisplayName: instance.displayName,
490
+ clientType: instance.clientType,
491
+ })));
492
+ }
493
+
494
+ async function resolveAuthorizedInbound(hellolynkId, delegateId, peerDelegateId = null) {
495
+ const acceptedHelloLynkId = normalizeContactHelloLynkId(hellolynkId);
496
+ normalizeDelegateId(delegateId);
497
+ const store = await readStore(storePath, profileId, protection);
498
+ const contact = store.contacts.find((item) => item.hellolynkId === acceptedHelloLynkId);
499
+ if (!contact) throw new HelloLynkLocalContactsError("contact_not_found");
500
+ const instance = peerDelegateId == null ? null : requireContactInstance(contact, normalizeDelegateId(peerDelegateId));
501
+ return {
502
+ contactId: contact.contactId,
503
+ displayName: contact.displayName,
504
+ authorization: "contact_delegate_allowlist",
505
+ ...(instance ? { ...resolvedInstance(contact, instance), senderDelegateId: instance.delegateId } : {}),
506
+ };
507
+ }
508
+
509
+ async function resolveAuthorizedSend(contactId, delegateId, peerDelegateId = null) {
510
+ const acceptedContactId = normalizeContactId(contactId);
511
+ const acceptedDelegateId = normalizeDelegateId(delegateId);
512
+ const store = await readStore(storePath, profileId, protection);
513
+ const contact = store.contacts.find((item) => item.contactId === acceptedContactId);
514
+ if (!contact) throw new HelloLynkLocalContactsError("contact_not_found");
515
+ if (!contact.autonomousSendDelegateIds.includes(acceptedDelegateId)) {
516
+ throw new HelloLynkLocalContactsError("contact_autonomous_send_not_authorized");
517
+ }
518
+ const instance = peerDelegateId == null ? null : requireAuthorizedContactInstance(contact,
519
+ normalizeDelegateId(peerDelegateId), acceptedDelegateId, "contact_autonomous_send_not_authorized");
520
+ return {
521
+ contactId: contact.contactId,
522
+ hellolynkId: contact.hellolynkId,
523
+ displayName: contact.displayName,
524
+ authorization: "contact_delegate_allowlist",
525
+ ...(instance ? resolvedInstance(contact, instance) : {}),
526
+ };
527
+ }
528
+
529
+ async function resolveAuthorizedReply(hellolynkId, delegateId, peerDelegateId) {
530
+ const acceptedHelloLynkId = normalizeContactHelloLynkId(hellolynkId);
531
+ const acceptedDelegateId = normalizeDelegateId(delegateId);
532
+ const acceptedPeerDelegateId = normalizeDelegateId(peerDelegateId);
533
+ // Admission linearizes at these checks under the contact mutation lock.
534
+ // The send grant comes from one snapshot. Release the lock before any network
535
+ // work: a later revocation cannot cancel a send already admitted here.
536
+ return mutateStore(storePath, profileId, async (store) => {
537
+ const contact = store.contacts.find((item) => item.hellolynkId === acceptedHelloLynkId);
538
+ if (!contact) throw new HelloLynkLocalContactsError("contact_not_found");
539
+ if (!contact.autonomousSendDelegateIds.includes(acceptedDelegateId)) {
540
+ throw new HelloLynkLocalContactsError("contact_autonomous_send_not_authorized");
541
+ }
542
+ const instance = requireAuthorizedContactInstance(contact, acceptedPeerDelegateId,
543
+ acceptedDelegateId, "contact_autonomous_send_not_authorized");
544
+ return noStoreWrite({ ...resolvedInstance(contact, instance), authorization: "contact_delegate_allowlist" });
545
+ }, protection);
546
+ }
547
+
548
+ async function revokeAutonomousMessagingByHelloLynkId(hellolynkId) {
549
+ const acceptedHelloLynkId = normalizeContactHelloLynkId(hellolynkId);
550
+ return mutateStore(storePath, profileId, async (store) => {
551
+ const contact = store.contacts.find((item) => item.hellolynkId === acceptedHelloLynkId);
552
+ if (!contact) return noStoreWrite(
553
+ Object.freeze({ contactFound: false, revokedDelegateCount: 0 }),
554
+ );
555
+ const revokedDelegateCount = new Set([
556
+ ...contact.autonomousSendDelegateIds,
557
+ ...contact.continuousReadDelegateIds,
558
+ ...contact.instances.flatMap((instance) => [
559
+ ...instance.autonomousSendDelegateIds,
560
+ ...instance.continuousReadDelegateIds,
561
+ ]),
562
+ ]).size;
563
+ if (revokedDelegateCount === 0) return noStoreWrite(
564
+ Object.freeze({ contactFound: true, revokedDelegateCount: 0 }),
565
+ );
566
+ contact.autonomousSendDelegateIds = [];
567
+ contact.continuousReadDelegateIds = [];
568
+ for (const instance of contact.instances) {
569
+ instance.autonomousSendDelegateIds = [];
570
+ instance.continuousReadDelegateIds = [];
571
+ if (instance.continuousReadAfterSeq) instance.continuousReadAfterSeq = [];
572
+ }
573
+ contact.updatedAt = normalizeTimestamp(now());
574
+ return Object.freeze({ contactFound: true, revokedDelegateCount });
575
+ }, protection);
576
+ }
577
+
578
+ return Object.freeze({
579
+ contractId: CONTACTS_CONTRACT,
580
+ path: storePath,
581
+ profileId,
582
+ snapshot,
583
+ list: async () => (await snapshot()).contacts,
584
+ listForDelegate,
585
+ listAuthorizedForDelegate,
586
+ listAuthorizedRoutesForDelegate,
587
+ listReadableRoutesForDelegate,
588
+ resolveAuthorizedInbound,
589
+ resolveAuthorizedSend,
590
+ resolveAuthorizedReply,
591
+ revokeAutonomousMessagingByHelloLynkId,
592
+ setAutonomousSendAuthorization,
593
+ getAiReadAuthorization,
594
+ upsertInstance,
595
+ renameInstance,
596
+ removeInstance,
597
+ resolveInstance,
598
+ save,
599
+ rename,
600
+ synchronizeRelationships,
601
+ delete: remove,
602
+ resolve,
603
+ });
604
+ }
605
+
606
+ function emptyStore(profileId) {
607
+ return {
608
+ schemaVersion: SCHEMA_VERSION,
609
+ contractId: CONTACTS_CONTRACT,
610
+ profileId,
611
+ contacts: [],
612
+ contactRequestReadDelegateIds: [],
613
+ relationshipSync: { owner: null, generations: [], pendingDeletedHelloLynkIds: [] },
614
+ };
615
+ }
616
+
617
+ function publicSnapshot(store, storePath) {
618
+ const contacts = store.contacts
619
+ .slice()
620
+ .sort((left, right) => left.displayName.localeCompare(right.displayName, "zh-CN"))
621
+ .map((contact) => publicContact(contact));
622
+ return {
623
+ schemaVersion: SCHEMA_VERSION,
624
+ contractId: CONTACTS_CONTRACT,
625
+ profileId: store.profileId,
626
+ contacts,
627
+ count: contacts.length,
628
+ storage: "local_file",
629
+ storePath,
630
+ };
631
+ }
632
+
633
+ function normalizeContactDataProtection(protector, profileId) {
634
+ if (protector == null) return null;
635
+ if (["sealJson", "openJson"].some((method) => typeof protector?.[method] !== "function")) {
636
+ throw new HelloLynkLocalContactsError("contact_data_protector_invalid");
637
+ }
638
+ return Object.freeze({
639
+ protector,
640
+ context: Object.freeze({
641
+ profileId,
642
+ assetKind: "contacts",
643
+ logicalPath: `contacts/${profileId}.json`,
644
+ }),
645
+ });
646
+ }
647
+
648
+ async function readStore(storePath, profileId, protection = null) {
649
+ await assertContactStoreAccess(storePath, protection);
650
+ await assertSafeStoreDirectories(storePath, { create: false });
651
+ const physicalMaximum = protection ? MAX_PROTECTED_STORE_BYTES : MAX_STORE_BYTES;
652
+ let metadata;
653
+ let handle;
654
+ try {
655
+ metadata = await lstat(storePath);
656
+ } catch (error) {
657
+ if (error?.code === "ENOENT") return emptyStore(profileId);
658
+ throw new HelloLynkLocalContactsError("contact_store_read_failed", { cause: error });
659
+ }
660
+ if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.size > physicalMaximum) {
661
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
662
+ }
663
+ let parsed;
664
+ try {
665
+ handle = await open(storePath, "r");
666
+ const openedMetadata = await handle.stat();
667
+ const windowsUnreportedDevice = process.platform === "win32"
668
+ && (metadata.dev === 0 || openedMetadata.dev === 0);
669
+ if (!openedMetadata.isFile() || openedMetadata.size > physicalMaximum
670
+ || (windowsUnreportedDevice && metadata.ino !== openedMetadata.ino)
671
+ || (Number.isSafeInteger(metadata.dev) && Number.isSafeInteger(metadata.ino)
672
+ && Number.isSafeInteger(openedMetadata.dev) && Number.isSafeInteger(openedMetadata.ino)
673
+ && (metadata.ino !== openedMetadata.ino
674
+ || (metadata.dev !== openedMetadata.dev
675
+ && !windowsUnreportedDevice)))) {
676
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
677
+ }
678
+ const stored = JSON.parse(await handle.readFile("utf8"));
679
+ parsed = protection
680
+ ? await protection.protector.openJson(stored, protection.context)
681
+ : stored;
682
+ if (serializedStoreBytes(parsed) > MAX_STORE_BYTES) {
683
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
684
+ }
685
+ } catch (error) {
686
+ throw new HelloLynkLocalContactsError("contact_store_invalid", { cause: error });
687
+ } finally {
688
+ await handle?.close().catch(() => {});
689
+ }
690
+ try {
691
+ return assertHelloLynkLocalContactStore(parsed, profileId);
692
+ } catch (error) {
693
+ throw new HelloLynkLocalContactsError("contact_store_invalid", { cause: error });
694
+ }
695
+ }
696
+
697
+ export function assertHelloLynkLocalContactStore(input, expectedProfileId) {
698
+ const profileId = normalizeProfileId(expectedProfileId);
699
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
700
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
701
+ }
702
+ assertExactObjectFields(
703
+ input,
704
+ ["schemaVersion", "contractId", "profileId", "contacts",
705
+ ...(input.schemaVersion >= 3 ? ["contactRequestReadDelegateIds"] : []),
706
+ ...(input.schemaVersion === SCHEMA_VERSION ? ["relationshipSync"] : [])],
707
+ );
708
+ if (!READABLE_SCHEMA_VERSIONS.has(input.schemaVersion) || input.contractId !== CONTACTS_CONTRACT
709
+ || input.profileId !== profileId || !Array.isArray(input.contacts)
710
+ || input.contacts.length > MAX_CONTACTS) {
711
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
712
+ }
713
+ const ids = new Set();
714
+ const stableAliases = new Set();
715
+ const names = new Set();
716
+ const contacts = input.contacts.map((contact) => {
717
+ assertExactObjectFields(contact, [
718
+ "contactId",
719
+ "hellolynkId",
720
+ "displayName",
721
+ ...(Object.hasOwn(contact, "principalId") ? ["principalId"] : []),
722
+ ...(Object.hasOwn(contact, "instances") ? ["instances"] : []),
723
+ "safetyCode",
724
+ ...(Object.hasOwn(contact, "safetyCodeVerificationState")
725
+ ? ["safetyCodeVerificationState"]
726
+ : []),
727
+ "autonomousSendDelegateIds",
728
+ ...(input.schemaVersion >= 3 ? ["continuousReadDelegateIds"] : []),
729
+ "createdAt",
730
+ "updatedAt",
731
+ ]);
732
+ const normalized = normalizeStoredContact(contact, input.schemaVersion);
733
+ const nameKey = contactDisplayNameKey(normalized.displayName);
734
+ const stableAlias = classifyHelloLynkStableId(normalized.hellolynkId)?.aliasKey;
735
+ if (ids.has(normalized.contactId) || stableAliases.has(stableAlias) || names.has(nameKey)) {
736
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
737
+ }
738
+ ids.add(normalized.contactId);
739
+ stableAliases.add(stableAlias);
740
+ names.add(nameKey);
741
+ return normalized;
742
+ });
743
+ return {
744
+ ...emptyStore(profileId),
745
+ contacts,
746
+ contactRequestReadDelegateIds: normalizeAuthorizedDelegateIds(input.contactRequestReadDelegateIds ?? []),
747
+ relationshipSync: structuredClone(runtimeSafety.normalizeHelloLynkRelationshipSync(
748
+ input.schemaVersion === SCHEMA_VERSION ? input.relationshipSync
749
+ : { owner: null, generations: [], pendingDeletedHelloLynkIds: [] })),
750
+ };
751
+ }
752
+
753
+ function clearPendingContactDeletion(store, hellolynkId) {
754
+ store.relationshipSync.pendingDeletedHelloLynkIds = store.relationshipSync.pendingDeletedHelloLynkIds
755
+ .filter((peer) => peer !== hellolynkId);
756
+ }
757
+
758
+ function rememberDeletedContact(store, hellolynkId) {
759
+ const sync = store.relationshipSync;
760
+ // Older manual records may include peers that this owner can never import.
761
+ // Deleting those remains local and does not need an ineligible sync marker.
762
+ if (sync.owner) {
763
+ const peer = classifyHelloLynkStableId(hellolynkId);
764
+ const owner = classifyHelloLynkStableId(sync.owner.hellolynkId);
765
+ if (peer.region !== owner.region || peer.aliasKey === owner.aliasKey) return;
766
+ }
767
+ const generations = sync.generations.filter((item) => item.hellolynkId === hellolynkId);
768
+ if (generations.length) {
769
+ for (const item of generations) item.suppressed = true;
770
+ } else if (!sync.pendingDeletedHelloLynkIds.includes(hellolynkId)) {
771
+ if (sync.generations.length + sync.pendingDeletedHelloLynkIds.length >= MAX_RELATIONSHIP_SYNC_RECORDS) {
772
+ throw new HelloLynkLocalContactsError("contact_relationship_sync_capacity_exceeded");
773
+ }
774
+ sync.pendingDeletedHelloLynkIds.push(hellolynkId);
775
+ }
776
+ }
777
+
778
+ function assertRelationshipPeer(hellolynkId, owner) {
779
+ const peer = classifyHelloLynkStableId(hellolynkId);
780
+ const identity = classifyHelloLynkStableId(owner.hellolynkId);
781
+ if (!peer || peer.region !== identity?.region || peer.aliasKey === identity.aliasKey) {
782
+ throw new HelloLynkLocalContactsError("contact_relationship_peer_invalid");
783
+ }
784
+ }
785
+
786
+ function normalizeRelationshipSynchronization(input) {
787
+ assertExactObjectFields(input, ["principalId", "hellolynkId", "relationships"]);
788
+ const owner = {
789
+ principalId: normalizeDelegateId(input.principalId),
790
+ hellolynkId: normalizeContactHelloLynkId(input.hellolynkId),
791
+ };
792
+ if (!Array.isArray(input.relationships)) throw new HelloLynkLocalContactsError("contact_relationships_invalid");
793
+ const ids = new Set();
794
+ const peers = new Set();
795
+ const relationships = input.relationships.map((relationship) => {
796
+ assertExactObjectFields(relationship, [
797
+ "relationshipId", "otherPrincipal", "status", "createdAt", "updatedAt", "endedAt",
798
+ ]);
799
+ const other = relationship.otherPrincipal;
800
+ assertExactObjectFields(other, ["displayName", "hellolynkId",
801
+ ...(other && Object.hasOwn(other, "hellolynkIdPurpose") ? ["hellolynkIdPurpose"] : [])]);
802
+ if ((Object.hasOwn(other, "hellolynkIdPurpose")
803
+ && other.hellolynkIdPurpose !== "human_identity_verification_only")
804
+ || typeof other.displayName !== "string"
805
+ || typeof relationship.relationshipId !== "string"
806
+ || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,240}$/.test(relationship.relationshipId)
807
+ || !["active", "ended"].includes(relationship.status)) {
808
+ throw new HelloLynkLocalContactsError("contact_relationships_invalid");
809
+ }
810
+ const peer = normalizeContactHelloLynkId(other.hellolynkId);
811
+ assertRelationshipPeer(peer, owner);
812
+ if (ids.has(relationship.relationshipId) || peers.has(peer)) {
813
+ throw new HelloLynkLocalContactsError("contact_relationships_invalid");
814
+ }
815
+ ids.add(relationship.relationshipId);
816
+ peers.add(peer);
817
+ for (const field of ["createdAt", "updatedAt", "endedAt"]) {
818
+ if (field === "endedAt" && relationship[field] === null) continue;
819
+ if (typeof relationship[field] !== "string" || !Number.isFinite(Date.parse(relationship[field]))) {
820
+ throw new HelloLynkLocalContactsError("contact_relationships_invalid");
821
+ }
822
+ }
823
+ if ((relationship.status === "active") !== (relationship.endedAt === null)) {
824
+ throw new HelloLynkLocalContactsError("contact_relationships_invalid");
825
+ }
826
+ return { ...relationship, otherPrincipal: { displayName: other.displayName, hellolynkId: peer } };
827
+ });
828
+ relationships.sort((left, right) => left.otherPrincipal.hellolynkId.localeCompare(right.otherPrincipal.hellolynkId, "en"));
829
+ return { owner, relationships };
830
+ }
831
+
832
+ function automaticContactDisplayName(value, hellolynkId, contacts) {
833
+ let base;
834
+ try { base = normalizeContactDisplayName(value); } catch { base = "联系人"; }
835
+ const names = new Set(contacts.map((contact) => contactDisplayNameKey(contact.displayName)));
836
+ if (!names.has(contactDisplayNameKey(base))) return base;
837
+ const digest = crypto.createHash("sha256").update(hellolynkId).digest("hex");
838
+ for (let attempt = 0; attempt <= MAX_CONTACTS; attempt += 1) {
839
+ const suffix = ` (${digest.slice(0, 8)}${attempt ? `-${attempt}` : ""})`;
840
+ const name = `${[...base].slice(0, 80 - suffix.length).join("")}${suffix}`;
841
+ if (!names.has(contactDisplayNameKey(name))) return name;
842
+ }
843
+ throw new HelloLynkLocalContactsError("contact_display_name_conflict");
844
+ }
845
+
846
+ function assertExactObjectFields(value, fields, code = "contact_store_invalid") {
847
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
848
+ throw new HelloLynkLocalContactsError(code);
849
+ }
850
+ const observed = Object.keys(value).sort();
851
+ const expected = [...fields].sort();
852
+ if (observed.length !== expected.length
853
+ || observed.some((field, index) => field !== expected[index])) {
854
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
855
+ }
856
+ }
857
+
858
+ function normalizeStoredContact(contact, schemaVersion) {
859
+ if (!contact || typeof contact !== "object" || Array.isArray(contact)) {
860
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
861
+ }
862
+ const safetyCode = normalizeContactSafetyCode(contact.safetyCode);
863
+ const principalId = contact.principalId == null ? null : normalizeDelegateId(contact.principalId);
864
+ const instances = normalizeContactInstances(contact.instances ?? [], schemaVersion);
865
+ if (instances.length && principalId == null) throw new HelloLynkLocalContactsError("contact_store_invalid");
866
+ return {
867
+ contactId: normalizeContactId(contact.contactId),
868
+ hellolynkId: normalizeContactHelloLynkId(contact.hellolynkId),
869
+ displayName: normalizeContactDisplayName(contact.displayName),
870
+ principalId,
871
+ instances,
872
+ safetyCode,
873
+ safetyCodeVerificationState: normalizeSafetyCodeVerificationState(
874
+ contact.safetyCodeVerificationState,
875
+ safetyCode,
876
+ Object.hasOwn(contact, "safetyCodeVerificationState"),
877
+ ),
878
+ autonomousSendDelegateIds: normalizeAuthorizedDelegateIds(contact.autonomousSendDelegateIds),
879
+ continuousReadDelegateIds: normalizeAuthorizedDelegateIds(contact.continuousReadDelegateIds ?? []),
880
+ createdAt: normalizeTimestamp(contact.createdAt),
881
+ updatedAt: normalizeTimestamp(contact.updatedAt),
882
+ };
883
+ }
884
+
885
+ function publicContact(contact) {
886
+ return {
887
+ contactId: contact.contactId,
888
+ hellolynkId: contact.hellolynkId,
889
+ displayName: contact.displayName,
890
+ principalId: contact.principalId,
891
+ instances: contact.instances.map((instance) => publicInstance(instance)),
892
+ createdAt: contact.createdAt,
893
+ updatedAt: contact.updatedAt,
894
+ };
895
+ }
896
+
897
+ function projectContactForDelegate(contact, delegateId) {
898
+ return {
899
+ ...publicContact(contact),
900
+ autonomousSendAuthorized: contact.autonomousSendDelegateIds.includes(delegateId),
901
+ continuousReadAuthorized: true,
902
+ instances: contact.instances.map((instance) => ({
903
+ ...publicInstance(instance),
904
+ autonomousSendAuthorized: contact.autonomousSendDelegateIds.includes(delegateId)
905
+ && instance.autonomousSendDelegateIds.includes(delegateId),
906
+ continuousReadAuthorized: true,
907
+ })),
908
+ };
909
+ }
910
+
911
+ function normalizeInstanceClientType(value) {
912
+ if (!INSTANCE_CLIENT_TYPES.has(value)) throw new HelloLynkLocalContactsError("contact_instance_client_type_invalid");
913
+ return value;
914
+ }
915
+
916
+ function normalizeContactInstances(value, schemaVersion) {
917
+ if (!Array.isArray(value) || value.length > MAX_INSTANCES_PER_CONTACT) {
918
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
919
+ }
920
+ const ids = new Set();
921
+ return value.map((instance) => {
922
+ assertExactObjectFields(instance, ["delegateId", "clientType", "displayName", "createdAt", "updatedAt", "autonomousSendDelegateIds",
923
+ ...(schemaVersion >= 3 ? ["continuousReadDelegateIds"] : []),
924
+ ...(Object.hasOwn(instance, "continuousReadAfterSeq") ? ["continuousReadAfterSeq"] : [])]);
925
+ const delegateId = normalizeDelegateId(instance.delegateId);
926
+ if (ids.has(delegateId)) throw new HelloLynkLocalContactsError("contact_store_invalid");
927
+ ids.add(delegateId);
928
+ return {
929
+ delegateId,
930
+ clientType: normalizeInstanceClientType(instance.clientType),
931
+ displayName: normalizeContactDisplayName(instance.displayName),
932
+ createdAt: normalizeTimestamp(instance.createdAt),
933
+ updatedAt: normalizeTimestamp(instance.updatedAt),
934
+ autonomousSendDelegateIds: normalizeAuthorizedDelegateIds(instance.autonomousSendDelegateIds),
935
+ continuousReadDelegateIds: normalizeAuthorizedDelegateIds(instance.continuousReadDelegateIds ?? []),
936
+ ...(Object.hasOwn(instance, "continuousReadAfterSeq") ? {
937
+ continuousReadAfterSeq: normalizeReadBoundaries(instance.continuousReadAfterSeq, instance.continuousReadDelegateIds),
938
+ } : {}),
939
+ };
940
+ });
941
+ }
942
+
943
+ function normalizeReadBoundaries(value, authorizedIds) {
944
+ if (!Array.isArray(value) || value.length > MAX_AUTHORIZED_DELEGATES_PER_CONTACT) {
945
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
946
+ }
947
+ const seen = new Set();
948
+ return value.map((entry) => {
949
+ assertExactObjectFields(entry, ["delegateId", "afterSeq"]);
950
+ const delegateId = normalizeDelegateId(entry.delegateId);
951
+ if (entry.delegateId !== delegateId || seen.has(delegateId) || !authorizedIds?.includes(delegateId)
952
+ || (entry.afterSeq !== null && (!Number.isSafeInteger(entry.afterSeq) || entry.afterSeq < 0))) {
953
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
954
+ }
955
+ seen.add(delegateId);
956
+ return { delegateId, afterSeq: entry.afterSeq };
957
+ });
958
+ }
959
+
960
+ function publicInstance(instance) {
961
+ return {
962
+ delegateId: instance.delegateId,
963
+ clientType: instance.clientType,
964
+ displayName: instance.displayName,
965
+ createdAt: instance.createdAt,
966
+ updatedAt: instance.updatedAt,
967
+ };
968
+ }
969
+
970
+ function requireContactInstance(contact, peerDelegateId) {
971
+ const instance = contact.instances.find((item) => item.delegateId === peerDelegateId);
972
+ if (!instance || contact.principalId == null) throw new HelloLynkLocalContactsError("contact_instance_not_found");
973
+ return instance;
974
+ }
975
+
976
+ function requireAuthorizedContactInstance(contact, peerDelegateId, localDelegateId, errorCode,
977
+ authorizationField = "autonomousSendDelegateIds") {
978
+ const instance = requireContactInstance(contact, peerDelegateId);
979
+ if (!instance[authorizationField].includes(localDelegateId)) {
980
+ throw new HelloLynkLocalContactsError(errorCode);
981
+ }
982
+ return instance;
983
+ }
984
+
985
+ function resolvedInstance(contact, instance) {
986
+ return {
987
+ contactId: contact.contactId,
988
+ hellolynkId: contact.hellolynkId,
989
+ principalId: contact.principalId,
990
+ displayName: contact.displayName,
991
+ recipientDelegateId: instance.delegateId,
992
+ instance: publicInstance(instance),
993
+ };
994
+ }
995
+
996
+ function normalizeAuthorizedDelegateIds(value) {
997
+ if (!Array.isArray(value) || value.length > MAX_AUTHORIZED_DELEGATES_PER_CONTACT) {
998
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
999
+ }
1000
+ const normalized = value.map(normalizeDelegateId);
1001
+ if (new Set(normalized).size !== normalized.length) throw new HelloLynkLocalContactsError("contact_store_invalid");
1002
+ return normalized.sort();
1003
+ }
1004
+
1005
+ function normalizeDelegateId(value) {
1006
+ const delegateId = String(value || "").trim();
1007
+ if (!DELEGATE_ID_RE.test(delegateId)) throw new HelloLynkLocalContactsError("contact_delegate_id_invalid");
1008
+ return delegateId;
1009
+ }
1010
+
1011
+ async function mutateStore(storePath, profileId, callback, protection = null) {
1012
+ const previous = mutationQueues.get(storePath) || Promise.resolve();
1013
+ let release;
1014
+ const current = new Promise((resolve) => { release = resolve; });
1015
+ const tail = previous.then(() => current);
1016
+ mutationQueues.set(storePath, tail);
1017
+ await previous;
1018
+ try {
1019
+ return await withFileLock(storePath, async () => {
1020
+ const store = await readStore(storePath, profileId, protection);
1021
+ const result = await callback(store);
1022
+ if (result?.[NO_STORE_WRITE] === true) return result.value;
1023
+ await atomicWriteStore(storePath, store, protection);
1024
+ return result;
1025
+ });
1026
+ } finally {
1027
+ release();
1028
+ if (mutationQueues.get(storePath) === tail) mutationQueues.delete(storePath);
1029
+ }
1030
+ }
1031
+
1032
+ function initialSafetyCodeVerificationState(safetyCode) {
1033
+ return safetyCode
1034
+ ? SAFETY_CODE_VERIFICATION_STATES.verificationRequired
1035
+ : SAFETY_CODE_VERIFICATION_STATES.unavailable;
1036
+ }
1037
+
1038
+ function normalizeSafetyCodeVerificationState(value, safetyCode, explicitlyStored) {
1039
+ const state = explicitlyStored ? String(value || "") : initialSafetyCodeVerificationState(safetyCode);
1040
+ if (!Object.values(SAFETY_CODE_VERIFICATION_STATES).includes(state)) {
1041
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
1042
+ }
1043
+ if ((safetyCode == null) !== (state === SAFETY_CODE_VERIFICATION_STATES.unavailable)) {
1044
+ throw new HelloLynkLocalContactsError("contact_store_invalid");
1045
+ }
1046
+ return state;
1047
+ }
1048
+
1049
+ function noStoreWrite(value) {
1050
+ return { [NO_STORE_WRITE]: true, value };
1051
+ }
1052
+
1053
+ async function withFileLock(storePath, callback) {
1054
+ const lockPath = `${storePath}.lock`;
1055
+ await assertSafeStoreDirectories(storePath, { create: true });
1056
+ let handle = null;
1057
+ let token = "";
1058
+ for (let attempt = 0; attempt < LOCK_ATTEMPTS; attempt += 1) {
1059
+ try {
1060
+ handle = await open(lockPath, "wx", 0o600);
1061
+ token = crypto.randomBytes(24).toString("hex");
1062
+ await handle.writeFile(`${JSON.stringify({ pid: process.pid, token, createdAt: Date.now() })}\n`, "utf8");
1063
+ await handle.sync();
1064
+ break;
1065
+ } catch (error) {
1066
+ await handle?.close().catch(() => {});
1067
+ handle = null;
1068
+ if (error?.code !== "EEXIST") throw error;
1069
+ await recoverDeadLock(lockPath);
1070
+ await delay(LOCK_WAIT_MS);
1071
+ }
1072
+ }
1073
+ if (!handle) throw new HelloLynkLocalContactsError("contact_store_busy");
1074
+ try {
1075
+ return await callback();
1076
+ } finally {
1077
+ await handle.close().catch(() => {});
1078
+ try {
1079
+ const current = JSON.parse(await readFile(lockPath, "utf8"));
1080
+ if (current?.token === token) await rm(lockPath);
1081
+ } catch {}
1082
+ }
1083
+ }
1084
+
1085
+ async function recoverDeadLock(lockPath) {
1086
+ let metadata;
1087
+ try {
1088
+ metadata = await lstat(lockPath);
1089
+ } catch (error) {
1090
+ if (error?.code === "ENOENT") return false;
1091
+ throw error;
1092
+ }
1093
+ if (metadata.isSymbolicLink() || !metadata.isFile() || Date.now() - metadata.mtimeMs < LOCK_STALE_MS) return false;
1094
+ let record;
1095
+ try {
1096
+ record = JSON.parse(await readFile(lockPath, "utf8"));
1097
+ } catch {
1098
+ return false;
1099
+ }
1100
+ const pid = Number(record?.pid);
1101
+ if (!Number.isSafeInteger(pid) || pid <= 0 || processAlive(pid)) return false;
1102
+ try {
1103
+ await rm(lockPath);
1104
+ return true;
1105
+ } catch (error) {
1106
+ if (["ENOENT", "EACCES", "EPERM"].includes(error?.code)) return false;
1107
+ throw error;
1108
+ }
1109
+ }
1110
+
1111
+ async function atomicWriteStore(storePath, store, protection = null) {
1112
+ await assertContactStoreAccess(storePath, protection);
1113
+ await assertSafeStoreDirectories(storePath, { create: true });
1114
+ if (serializedStoreBytes(store) > MAX_STORE_BYTES) {
1115
+ throw new HelloLynkLocalContactsError("contact_store_too_large");
1116
+ }
1117
+ const stored = protection
1118
+ ? await protection.protector.sealJson(store, protection.context)
1119
+ : store;
1120
+ const body = `${JSON.stringify(stored, null, 2)}\n`;
1121
+ if (Buffer.byteLength(body, "utf8") > (protection ? MAX_PROTECTED_STORE_BYTES : MAX_STORE_BYTES)) {
1122
+ throw new HelloLynkLocalContactsError("contact_store_too_large");
1123
+ }
1124
+ const temporary = `${storePath}.${crypto.randomUUID()}.tmp`;
1125
+ const handle = await open(temporary, "wx", 0o600);
1126
+ try {
1127
+ await handle.writeFile(body, "utf8");
1128
+ await handle.sync();
1129
+ } finally {
1130
+ await handle.close();
1131
+ }
1132
+ try {
1133
+ await assertContactStoreAccess(storePath, protection);
1134
+ let lastError;
1135
+ for (const wait of [0, 25, 75, 200]) {
1136
+ if (wait) await delay(wait);
1137
+ try {
1138
+ await rename(temporary, storePath);
1139
+ return;
1140
+ } catch (error) {
1141
+ lastError = error;
1142
+ if (!["EACCES", "EPERM", "EBUSY"].includes(error?.code)) throw error;
1143
+ }
1144
+ }
1145
+ throw lastError;
1146
+ } catch (error) {
1147
+ await rm(temporary, { force: true });
1148
+ throw new HelloLynkLocalContactsError("contact_store_write_failed", { cause: error });
1149
+ }
1150
+ }
1151
+
1152
+ async function assertContactStoreAccess(storePath, protection) {
1153
+ const contactsDirectory = path.dirname(path.resolve(storePath));
1154
+ const dataRoot = path.basename(contactsDirectory) === "contacts"
1155
+ ? path.dirname(contactsDirectory)
1156
+ : contactsDirectory;
1157
+ try {
1158
+ await assertLocalDataProtectionAccess(dataRoot, protection != null);
1159
+ } catch (error) {
1160
+ if (error instanceof HelloLynkLocalContactsError) throw error;
1161
+ throw new HelloLynkLocalContactsError(error?.code || "contact_store_access_invalid", { cause: error });
1162
+ }
1163
+ }
1164
+
1165
+ function serializedStoreBytes(store) {
1166
+ return Buffer.byteLength(`${JSON.stringify(store, null, 2)}\n`, "utf8");
1167
+ }
1168
+
1169
+ async function assertSafeStoreDirectories(storePath, options = {}) {
1170
+ const contactsDir = path.dirname(storePath);
1171
+ const rootDir = path.dirname(contactsDir);
1172
+ const create = options.create === true;
1173
+ await assertSafeDirectory(rootDir, { create, recursive: true });
1174
+ await assertSafeDirectory(contactsDir, { create, recursive: false });
1175
+ }
1176
+
1177
+ async function assertSafeDirectory(directoryPath, options = {}) {
1178
+ let metadata;
1179
+ try {
1180
+ metadata = await lstat(directoryPath);
1181
+ } catch (error) {
1182
+ if (error?.code !== "ENOENT") {
1183
+ throw new HelloLynkLocalContactsError("contact_store_directory_invalid", { cause: error });
1184
+ }
1185
+ if (!options.create) return false;
1186
+ try {
1187
+ await mkdir(directoryPath, { recursive: options.recursive === true });
1188
+ metadata = await lstat(directoryPath);
1189
+ } catch (createError) {
1190
+ throw new HelloLynkLocalContactsError("contact_store_directory_invalid", { cause: createError });
1191
+ }
1192
+ }
1193
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
1194
+ throw new HelloLynkLocalContactsError("contact_store_directory_invalid");
1195
+ }
1196
+ return true;
1197
+ }
1198
+
1199
+ function normalizeProfileId(value) {
1200
+ const profileId = String(value || "").trim();
1201
+ if (!PROFILE_ID_RE.test(profileId)) throw new HelloLynkLocalContactsError("contact_profile_id_invalid");
1202
+ return profileId;
1203
+ }
1204
+
1205
+ function normalizeContactId(value) {
1206
+ const contactId = String(value || "").trim();
1207
+ if (!CONTACT_ID_RE.test(contactId)) throw new HelloLynkLocalContactsError("contact_id_invalid");
1208
+ return contactId;
1209
+ }
1210
+
1211
+ function normalizeTimestamp(value) {
1212
+ const parsed = value instanceof Date ? value : new Date(value);
1213
+ if (!Number.isFinite(parsed.valueOf())) throw new HelloLynkLocalContactsError("contact_timestamp_invalid");
1214
+ return parsed.toISOString();
1215
+ }
1216
+
1217
+ function processAlive(pid) {
1218
+ try {
1219
+ process.kill(pid, 0);
1220
+ return true;
1221
+ } catch (error) {
1222
+ return error?.code === "EPERM";
1223
+ }
1224
+ }
1225
+
1226
+ function delay(milliseconds) {
1227
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
1228
+ }