@hasna/contacts 0.8.1 → 0.9.1

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 (56) hide show
  1. package/README.md +10 -0
  2. package/dist/browser/cli.d.ts +4 -0
  3. package/dist/browser/cli.d.ts.map +1 -0
  4. package/dist/browser/identity.d.ts +3 -0
  5. package/dist/browser/identity.d.ts.map +1 -0
  6. package/dist/browser/install.d.ts +32 -0
  7. package/dist/browser/install.d.ts.map +1 -0
  8. package/dist/browser/native.d.ts +3 -0
  9. package/dist/browser/native.d.ts.map +1 -0
  10. package/dist/browser/native.js +822 -0
  11. package/dist/browser/protocol.d.ts +26 -0
  12. package/dist/browser/protocol.d.ts.map +1 -0
  13. package/dist/browser/values.d.ts +18 -0
  14. package/dist/browser/values.d.ts.map +1 -0
  15. package/dist/cli/commands/core.d.ts.map +1 -1
  16. package/dist/cli/index.js +408 -127
  17. package/dist/cli/status-domain.preload.d.ts +1 -1
  18. package/dist/cli/status-domain.preload.d.ts.map +1 -1
  19. package/dist/cli/status-fixture.d.ts +17 -0
  20. package/dist/cli/status-fixture.d.ts.map +1 -0
  21. package/dist/index.js +43 -4
  22. package/dist/lib/audience-contract.d.ts +9 -9
  23. package/dist/lib/compact-output.d.ts +67 -0
  24. package/dist/lib/compact-output.d.ts.map +1 -0
  25. package/dist/mcp/handlers/core.d.ts.map +1 -1
  26. package/dist/mcp/index.d.ts +2 -1
  27. package/dist/mcp/index.d.ts.map +1 -1
  28. package/dist/mcp/index.js +378 -194
  29. package/dist/mcp/profile.d.ts +8 -0
  30. package/dist/mcp/profile.d.ts.map +1 -0
  31. package/dist/mcp/register-tools.d.ts +3 -1
  32. package/dist/mcp/register-tools.d.ts.map +1 -1
  33. package/dist/mcp/storage-tools.d.ts +2 -1
  34. package/dist/mcp/storage-tools.d.ts.map +1 -1
  35. package/dist/mcp/tools.d.ts +7 -45
  36. package/dist/mcp/tools.d.ts.map +1 -1
  37. package/dist/sdk/index.js +1 -1
  38. package/dist/server/index.js +30 -16
  39. package/dist/server/pg-store.d.ts +1 -0
  40. package/dist/server/pg-store.d.ts.map +1 -1
  41. package/dist/store/index.d.ts.map +1 -1
  42. package/docs/chrome-autofill.md +45 -0
  43. package/extension/background.js +284 -0
  44. package/extension/content.js +392 -0
  45. package/extension/detect.js +228 -0
  46. package/extension/fill.js +112 -0
  47. package/extension/icons/128.png +0 -0
  48. package/extension/icons/16.png +0 -0
  49. package/extension/icons/32.png +0 -0
  50. package/extension/icons/48.png +0 -0
  51. package/extension/icons/icon.svg +7 -0
  52. package/extension/manifest.json +54 -0
  53. package/extension/popup.css +71 -0
  54. package/extension/popup.html +50 -0
  55. package/extension/popup.js +166 -0
  56. package/package.json +9 -5
@@ -0,0 +1,822 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/browser/native.ts
5
+ import { createRequire } from "module";
6
+
7
+ // src/sdk/index.ts
8
+ import { resolveCredential as resolveCredential2 } from "@hasna/contracts/client";
9
+
10
+ // src/cloud/http-storage.ts
11
+ import {
12
+ ClientTransportConfigurationError,
13
+ createHasnaHttpTransport,
14
+ resolveCredential,
15
+ resolveClientTransport as resolveSharedClientTransport
16
+ } from "@hasna/contracts/client";
17
+
18
+ // src/cloud/client-config.ts
19
+ import { createHash } from "crypto";
20
+ import { readFileSync, statSync } from "fs";
21
+ import { clientTransportEnvKeys, credentialDiskSourceList, toV1BaseUrl } from "@hasna/contracts/client";
22
+ function invalid() {
23
+ throw new Error("CONTACTS_CLIENT_CONFIG_INVALID: blank, conflicting, or unstable client configuration; no request was sent.");
24
+ }
25
+ function checkAliases(values) {
26
+ const keys = clientTransportEnvKeys("contacts");
27
+ for (const group of [keys.apiUrlKeys, keys.apiKeyKeys]) {
28
+ const defined = group.filter((key) => values[key] !== undefined);
29
+ const normalized = defined.map((key) => {
30
+ const value = values[key].trim();
31
+ if (!value)
32
+ invalid();
33
+ if (group === keys.apiUrlKeys) {
34
+ try {
35
+ return toV1BaseUrl(value);
36
+ } catch {
37
+ invalid();
38
+ }
39
+ }
40
+ return value;
41
+ });
42
+ if (new Set(normalized).size > 1)
43
+ invalid();
44
+ }
45
+ }
46
+ function checkDiskAliases(text) {
47
+ const values = {};
48
+ const wanted = new Set(Object.values(clientTransportEnvKeys("contacts")).flat());
49
+ for (const line of text.split(/\r?\n/)) {
50
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/.exec(line);
51
+ if (!match || !wanted.has(match[1]))
52
+ continue;
53
+ let value = match[2].trim();
54
+ if (value.startsWith('"') || value.startsWith("'")) {
55
+ if (value.length < 2 || value.at(-1) !== value[0])
56
+ invalid();
57
+ value = value.slice(1, -1);
58
+ }
59
+ if (values[match[1]] !== undefined && values[match[1]] !== value)
60
+ invalid();
61
+ values[match[1]] = value;
62
+ }
63
+ checkAliases(values);
64
+ }
65
+ function clientConfigurationStamp(env) {
66
+ checkAliases(env);
67
+ const digest = createHash("sha256");
68
+ digest.update(JSON.stringify(Object.entries(env).sort(([a], [b]) => a.localeCompare(b))));
69
+ const profile = env.HASNA_PROFILE?.trim();
70
+ if (profile && !/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(profile))
71
+ invalid();
72
+ const paths = [
73
+ ...credentialDiskSourceList("contacts", env),
74
+ ...profile ? credentialDiskSourceList("contacts", env, profile) : []
75
+ ];
76
+ for (const { path } of paths) {
77
+ digest.update(path);
78
+ try {
79
+ const before = statSync(path, { bigint: true });
80
+ if (!before.isFile() || before.size > 65536n)
81
+ invalid();
82
+ const contents = readFileSync(path);
83
+ const after = statSync(path, { bigint: true });
84
+ const identity = (s) => `${s.dev}:${s.ino}:${s.size}:${s.mtimeNs}:${s.ctimeNs}`;
85
+ if (identity(before) !== identity(after))
86
+ invalid();
87
+ checkDiskAliases(contents.toString("utf8"));
88
+ digest.update(identity(after)).update(contents);
89
+ } catch (error) {
90
+ if (error.code !== "ENOENT")
91
+ invalid();
92
+ digest.update("absent");
93
+ }
94
+ }
95
+ return digest.digest("hex");
96
+ }
97
+ function assertConfigurationUnchanged(env, expected) {
98
+ if (clientConfigurationStamp(env) !== expected)
99
+ invalid();
100
+ }
101
+
102
+ // src/cloud/resolver-inputs.ts
103
+ import {
104
+ clientTransportEnvKeys as clientTransportEnvKeys2,
105
+ credentialOverrideEnvKey,
106
+ credentialPointerEnvKey,
107
+ CREDENTIAL_PROFILE_ENV_KEY
108
+ } from "@hasna/contracts/client";
109
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
110
+ function isAmbientContactsEnv(env) {
111
+ if (typeof process !== "undefined" && env === process.env)
112
+ return true;
113
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
114
+ }
115
+ function contactsResolverCredentials(env, credentials = {}) {
116
+ const keychain = { ...credentials.keychain };
117
+ if (keychain.enabled === undefined && keychain.run === undefined) {
118
+ keychain.enabled = isAmbientContactsEnv(env);
119
+ }
120
+ return { ...credentials, keychain };
121
+ }
122
+
123
+ // src/cloud/http-storage.ts
124
+ var RETIRED_CLIENT_SELECTOR_KEYS = [
125
+ "HASNA_CONTACTS_STORAGE_MODE",
126
+ "CONTACTS_STORAGE_MODE",
127
+ "HASNA_CONTACTS_MODE",
128
+ "CONTACTS_MODE",
129
+ "HASNA_CONTACTS_DB_PATH",
130
+ "CONTACTS_DB_PATH",
131
+ "HASNA_CONTACTS_DATABASE_URL",
132
+ "CONTACTS_DATABASE_URL"
133
+ ];
134
+ function configuredKeys(env, keys) {
135
+ return keys.filter((key) => env[key] !== undefined && env[key].trim().length > 0);
136
+ }
137
+ function assertNoRetiredClientSelectors(env) {
138
+ const found = configuredKeys(env, RETIRED_CLIENT_SELECTOR_KEYS);
139
+ if (found.length === 0)
140
+ return;
141
+ throw new ContactsClientConfigurationError("RETIRED_CONTACTS_CLIENT_SELECTOR", `Contacts clients use only HASNA_CONTACTS_API_URL plus an API key resolved by @hasna/contracts. ` + `Remove retired client selector${found.length === 1 ? "" : "s"}: ${found.join(", ")}. ` + "PostgreSQL configuration belongs only on contacts-serve; local SQLite is available only through the explicit legacy migration command.");
142
+ }
143
+ function assertHttpsBaseUrl(baseUrl) {
144
+ const url = new URL(baseUrl);
145
+ if (url.protocol !== "https:") {
146
+ throw new ContactsClientConfigurationError("CONTACTS_API_HTTPS_REQUIRED", "HASNA_CONTACTS_API_URL must use HTTPS. Plain HTTP and local-store fallback are disabled for contacts clients.");
147
+ }
148
+ }
149
+
150
+ class ContactsClientConfigurationError extends Error {
151
+ code;
152
+ constructor(code, message) {
153
+ super(`${code}: ${message}`);
154
+ this.code = code;
155
+ this.name = "ContactsClientConfigurationError";
156
+ }
157
+ }
158
+ function unconfiguredResolution(issue, resolution, warning = null) {
159
+ return {
160
+ transport: "unconfigured",
161
+ baseUrl: null,
162
+ apiUrlSource: resolution?.apiUrlSource ?? null,
163
+ apiKeyPresent: resolution?.apiKeyPresent ?? false,
164
+ apiKeySource: resolution?.apiKeySource ?? null,
165
+ apiKeyTier: resolution?.apiKeyTier ?? null,
166
+ configured: false,
167
+ misconfigured: true,
168
+ issue,
169
+ warning
170
+ };
171
+ }
172
+ function resolveContactsClientTransport(name, env = process.env, credentials = {}) {
173
+ if (name !== "contacts") {
174
+ throw new ContactsClientConfigurationError("CONTACTS_CLIENT_NAME_INVALID", "This resolver only accepts the contacts app slug.");
175
+ }
176
+ assertNoRetiredClientSelectors(env);
177
+ const stamp = clientConfigurationStamp(env);
178
+ let resolution;
179
+ try {
180
+ resolution = resolveSharedClientTransport(name, env, {
181
+ credentials: contactsResolverCredentials(env, credentials)
182
+ });
183
+ } catch (error) {
184
+ assertConfigurationUnchanged(env, stamp);
185
+ if (error instanceof ClientTransportConfigurationError) {
186
+ return unconfiguredResolution(error.message, null);
187
+ }
188
+ throw error;
189
+ }
190
+ assertConfigurationUnchanged(env, stamp);
191
+ try {
192
+ assertHttpsBaseUrl(resolution.baseUrl);
193
+ } catch (error) {
194
+ return {
195
+ ...unconfiguredResolution(error instanceof Error ? error.message : String(error), resolution),
196
+ apiKeyPresent: resolution.apiKeyPresent
197
+ };
198
+ }
199
+ return {
200
+ transport: "https",
201
+ baseUrl: resolution.baseUrl,
202
+ apiUrlSource: resolution.apiUrlSource,
203
+ apiKeyPresent: resolution.apiKeyPresent,
204
+ apiKeySource: resolution.apiKeySource,
205
+ apiKeyTier: resolution.apiKeyTier,
206
+ configured: true,
207
+ misconfigured: false,
208
+ issue: null,
209
+ warning: resolution.warning
210
+ };
211
+ }
212
+
213
+ // src/sdk/v1.generated.ts
214
+ class ApiError extends Error {
215
+ status;
216
+ body;
217
+ constructor(status, message, body) {
218
+ super(message);
219
+ this.status = status;
220
+ this.body = body;
221
+ this.name = "ApiError";
222
+ }
223
+ }
224
+
225
+ class ContactsV1Client {
226
+ baseUrl;
227
+ apiKey;
228
+ fetchImpl;
229
+ baseHeaders;
230
+ constructor(options) {
231
+ if (!options.baseUrl)
232
+ throw new Error("ContactsV1Client requires a baseUrl.");
233
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
234
+ this.apiKey = options.apiKey;
235
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
236
+ this.baseHeaders = options.headers ?? {};
237
+ }
238
+ async request(method, path, opts) {
239
+ const url = new URL(this.baseUrl + path);
240
+ if (opts.query) {
241
+ for (const [key, value] of Object.entries(opts.query)) {
242
+ if (value !== undefined && value !== null)
243
+ url.searchParams.set(key, String(value));
244
+ }
245
+ }
246
+ const headers = { Accept: "application/json", ...this.baseHeaders, ...opts.init?.headers };
247
+ if (this.apiKey)
248
+ headers["x-api-key"] = this.apiKey;
249
+ let payload;
250
+ if (opts.body !== undefined) {
251
+ headers["Content-Type"] = "application/json";
252
+ payload = JSON.stringify(opts.body);
253
+ }
254
+ const response = await this.fetchImpl(url.toString(), { ...opts.init, method, headers, body: payload });
255
+ const text = await response.text();
256
+ const data = text ? (() => {
257
+ try {
258
+ return JSON.parse(text);
259
+ } catch {
260
+ return text;
261
+ }
262
+ })() : undefined;
263
+ if (!response.ok) {
264
+ throw new ApiError(response.status, `${method} ${path} failed: ${response.status}`, data);
265
+ }
266
+ return data;
267
+ }
268
+ async listCompanies(query, init) {
269
+ return this.request("GET", `/v1/companies`, {
270
+ body: undefined,
271
+ query,
272
+ init
273
+ });
274
+ }
275
+ async createCompany(body, init) {
276
+ return this.request("POST", `/v1/companies`, {
277
+ body,
278
+ query: undefined,
279
+ init
280
+ });
281
+ }
282
+ async getCompany(id, init) {
283
+ return this.request("GET", `/v1/companies/${encodeURIComponent(String(id))}`, {
284
+ body: undefined,
285
+ query: undefined,
286
+ init
287
+ });
288
+ }
289
+ async deleteCompany(id, init) {
290
+ return this.request("DELETE", `/v1/companies/${encodeURIComponent(String(id))}`, {
291
+ body: undefined,
292
+ query: undefined,
293
+ init
294
+ });
295
+ }
296
+ async updateCompany(id, body, init) {
297
+ return this.request("PATCH", `/v1/companies/${encodeURIComponent(String(id))}`, {
298
+ body,
299
+ query: undefined,
300
+ init
301
+ });
302
+ }
303
+ async listContacts(query, init) {
304
+ return this.request("GET", `/v1/contacts`, {
305
+ body: undefined,
306
+ query,
307
+ init
308
+ });
309
+ }
310
+ async createContact(body, init) {
311
+ return this.request("POST", `/v1/contacts`, {
312
+ body,
313
+ query: undefined,
314
+ init
315
+ });
316
+ }
317
+ async getContactProjectIds(contactId, init) {
318
+ return this.request("GET", `/v1/contacts/${encodeURIComponent(String(contactId))}/projects`, {
319
+ body: undefined,
320
+ query: undefined,
321
+ init
322
+ });
323
+ }
324
+ async setContactProjects(contactId, body, init) {
325
+ return this.request("PUT", `/v1/contacts/${encodeURIComponent(String(contactId))}/projects`, {
326
+ body,
327
+ query: undefined,
328
+ init
329
+ });
330
+ }
331
+ async linkContactToProject(contactId, projectId, init) {
332
+ return this.request("PUT", `/v1/contacts/${encodeURIComponent(String(contactId))}/projects/${encodeURIComponent(String(projectId))}`, {
333
+ body: undefined,
334
+ query: undefined,
335
+ init
336
+ });
337
+ }
338
+ async unlinkContactFromProject(contactId, projectId, init) {
339
+ return this.request("DELETE", `/v1/contacts/${encodeURIComponent(String(contactId))}/projects/${encodeURIComponent(String(projectId))}`, {
340
+ body: undefined,
341
+ query: undefined,
342
+ init
343
+ });
344
+ }
345
+ async addTagToContact(contactId, tagId, init) {
346
+ return this.request("PUT", `/v1/contacts/${encodeURIComponent(String(contactId))}/tags/${encodeURIComponent(String(tagId))}`, {
347
+ body: undefined,
348
+ query: undefined,
349
+ init
350
+ });
351
+ }
352
+ async removeTagFromContact(contactId, tagId, init) {
353
+ return this.request("DELETE", `/v1/contacts/${encodeURIComponent(String(contactId))}/tags/${encodeURIComponent(String(tagId))}`, {
354
+ body: undefined,
355
+ query: undefined,
356
+ init
357
+ });
358
+ }
359
+ async getContact(id, init) {
360
+ return this.request("GET", `/v1/contacts/${encodeURIComponent(String(id))}`, {
361
+ body: undefined,
362
+ query: undefined,
363
+ init
364
+ });
365
+ }
366
+ async deleteContact(id, init) {
367
+ return this.request("DELETE", `/v1/contacts/${encodeURIComponent(String(id))}`, {
368
+ body: undefined,
369
+ query: undefined,
370
+ init
371
+ });
372
+ }
373
+ async updateContact(id, body, init) {
374
+ return this.request("PATCH", `/v1/contacts/${encodeURIComponent(String(id))}`, {
375
+ body,
376
+ query: undefined,
377
+ init
378
+ });
379
+ }
380
+ async listContactProjectMemberships(projectId, query, init) {
381
+ return this.request("GET", `/v1/projects/${encodeURIComponent(String(projectId))}/contact-memberships`, {
382
+ body: undefined,
383
+ query,
384
+ init
385
+ });
386
+ }
387
+ async readContactProjectMembership(projectId, contactId, init) {
388
+ return this.request("GET", `/v1/projects/${encodeURIComponent(String(projectId))}/contact-memberships/${encodeURIComponent(String(contactId))}`, {
389
+ body: undefined,
390
+ query: undefined,
391
+ init
392
+ });
393
+ }
394
+ async attachContactProjectMembership(projectId, contactId, body, init) {
395
+ return this.request("POST", `/v1/projects/${encodeURIComponent(String(projectId))}/contact-memberships/${encodeURIComponent(String(contactId))}/attach`, {
396
+ body,
397
+ query: undefined,
398
+ init
399
+ });
400
+ }
401
+ async detachContactProjectMembership(projectId, contactId, body, init) {
402
+ return this.request("POST", `/v1/projects/${encodeURIComponent(String(projectId))}/contact-memberships/${encodeURIComponent(String(contactId))}/detach`, {
403
+ body,
404
+ query: undefined,
405
+ init
406
+ });
407
+ }
408
+ async listContactIdsByProject(projectId, init) {
409
+ return this.request("GET", `/v1/projects/${encodeURIComponent(String(projectId))}/contacts`, {
410
+ body: undefined,
411
+ query: undefined,
412
+ init
413
+ });
414
+ }
415
+ async getStats(init) {
416
+ return this.request("GET", `/v1/stats`, {
417
+ body: undefined,
418
+ query: undefined,
419
+ init
420
+ });
421
+ }
422
+ async listTags(query, init) {
423
+ return this.request("GET", `/v1/tags`, {
424
+ body: undefined,
425
+ query,
426
+ init
427
+ });
428
+ }
429
+ async createTag(body, init) {
430
+ return this.request("POST", `/v1/tags`, {
431
+ body,
432
+ query: undefined,
433
+ init
434
+ });
435
+ }
436
+ async getTag(id, init) {
437
+ return this.request("GET", `/v1/tags/${encodeURIComponent(String(id))}`, {
438
+ body: undefined,
439
+ query: undefined,
440
+ init
441
+ });
442
+ }
443
+ async deleteTag(id, init) {
444
+ return this.request("DELETE", `/v1/tags/${encodeURIComponent(String(id))}`, {
445
+ body: undefined,
446
+ query: undefined,
447
+ init
448
+ });
449
+ }
450
+ async updateTag(id, body, init) {
451
+ return this.request("PATCH", `/v1/tags/${encodeURIComponent(String(id))}`, {
452
+ body,
453
+ query: undefined,
454
+ init
455
+ });
456
+ }
457
+ }
458
+
459
+ // src/sdk/index.ts
460
+ function validateBaseUrl(raw) {
461
+ let url;
462
+ try {
463
+ url = new URL(raw);
464
+ } catch {
465
+ throw new Error("ContactsV1Client baseUrl must be an absolute HTTPS URL.");
466
+ }
467
+ if (url.protocol !== "https:")
468
+ throw new Error("ContactsV1Client baseUrl must use HTTPS.");
469
+ if (url.username || url.password)
470
+ throw new Error("ContactsV1Client baseUrl must not contain credentials.");
471
+ if (url.search || url.hash)
472
+ throw new Error("ContactsV1Client baseUrl must not contain a query or fragment.");
473
+ url.pathname = url.pathname.replace(/\/+$/, "").replace(/\/v1$/, "");
474
+ return url.toString().replace(/\/+$/, "");
475
+ }
476
+ function validateApiKey(apiKey) {
477
+ const key = (apiKey ?? "").trim();
478
+ if (!key)
479
+ throw new Error("ContactsV1Client requires an API key.");
480
+ if (/[^\t\x20-\x7e]/.test(key)) {
481
+ throw new Error("ContactsV1Client API key contains bytes that are invalid in an HTTP header.");
482
+ }
483
+ return key;
484
+ }
485
+
486
+ class ContactsV1Client2 extends ContactsV1Client {
487
+ constructor(options) {
488
+ const fetchImpl = options.fetch ?? globalThis.fetch;
489
+ super({
490
+ ...options,
491
+ baseUrl: validateBaseUrl(options.baseUrl),
492
+ apiKey: validateApiKey(options.apiKey),
493
+ fetch: (input, init) => fetchImpl(input, { ...init, redirect: "manual" })
494
+ });
495
+ }
496
+ }
497
+ var CONTACTS_APP_NAME = "contacts";
498
+ function contactsSdkAuthorityPinMessage() {
499
+ return "an explicit baseUrl requires an explicit apiKey. The SDK never attaches a credential that " + "resolved for a different authority: pass `apiKey` explicitly, or omit `baseUrl` and let the " + "@hasna/contracts chain resolve both halves together.";
500
+ }
501
+ var SDK_HOSTED_ONLY = "The contacts SDK is hosted-only and never falls back to local data.";
502
+ function resolveSdkCredential(env, chainOptions) {
503
+ const credential = resolveCredential2(CONTACTS_APP_NAME, env, chainOptions);
504
+ if (!credential) {
505
+ const diagnosis = resolveContactsClientTransport(CONTACTS_APP_NAME, env, chainOptions);
506
+ throw new ContactsClientConfigurationError("CONTACTS_API_NOT_CONFIGURED", `${diagnosis.issue ?? "No contacts credential resolved."} ${SDK_HOSTED_ONLY}`);
507
+ }
508
+ if (credential.tier === "pointer") {
509
+ throw new ContactsClientConfigurationError("CONTACTS_CREDENTIAL_POINTER_UNSUPPORTED", `The contacts SDK resolves credentials synchronously and cannot complete the secrets-vault pointer ` + `${credential.source} per request. Use a literal tier instead: an explicit apiKey, the Keychain item ` + `hasna.credentials.${CONTACTS_APP_NAME}.api-key, ~/.hasna/${CONTACTS_APP_NAME}/config/credentials, ` + `or HASNA_CONTACTS_API_KEY.`);
510
+ }
511
+ return credential.apiKey;
512
+ }
513
+ function createContactsClient(options = {}) {
514
+ const { baseUrl, apiKey, env, profile, keychain, ...clientOptions } = options;
515
+ if (baseUrl !== undefined) {
516
+ if (!apiKey)
517
+ throw new ContactsClientConfigurationError("CONTACTS_CREDENTIAL_PINNED", contactsSdkAuthorityPinMessage());
518
+ return new ContactsV1Client2({ ...clientOptions, baseUrl, apiKey });
519
+ }
520
+ const envObject = env ?? (typeof process !== "undefined" ? process.env : {});
521
+ const requested = {
522
+ ...apiKey !== undefined ? { apiKey } : {},
523
+ ...profile !== undefined ? { profile } : {},
524
+ ...keychain !== undefined ? { keychain } : {}
525
+ };
526
+ const chainOptions = contactsResolverCredentials(envObject, requested);
527
+ const initialKey = resolveSdkCredential(envObject, chainOptions);
528
+ const resolution = resolveContactsClientTransport(CONTACTS_APP_NAME, envObject, { ...chainOptions, apiKey: initialKey });
529
+ if (!resolution.configured || !resolution.baseUrl) {
530
+ throw new ContactsClientConfigurationError("CONTACTS_API_NOT_CONFIGURED", `${resolution.issue ?? "The contacts API client is not configured."} ${SDK_HOSTED_ONLY}`);
531
+ }
532
+ const pinnedBaseUrl = resolution.baseUrl;
533
+ const baseFetch = clientOptions.fetch ?? globalThis.fetch;
534
+ const chainFetch = async (input, init) => {
535
+ const snapshot = { ...envObject };
536
+ const freshKey = resolveSdkCredential(snapshot, chainOptions);
537
+ const current = resolveContactsClientTransport(CONTACTS_APP_NAME, snapshot, { ...chainOptions, apiKey: freshKey });
538
+ if (!current.configured || current.baseUrl !== pinnedBaseUrl) {
539
+ throw new ContactsClientConfigurationError("CONTACTS_AUTHORITY_CHANGED", "Client authority changed or disappeared; construct a new client before sending data.");
540
+ }
541
+ const headers = new Headers(init?.headers);
542
+ headers.set("x-api-key", freshKey);
543
+ return baseFetch(input, { ...init, headers });
544
+ };
545
+ return new ContactsV1Client2({ ...clientOptions, baseUrl: pinnedBaseUrl, apiKey: initialKey, fetch: chainFetch });
546
+ }
547
+
548
+ // src/browser/identity.ts
549
+ var EXTENSION_ID = "ceegpmbcoiomonopbhpifdalooccfgjj";
550
+
551
+ // src/browser/protocol.ts
552
+ class BrowserError extends Error {
553
+ code;
554
+ constructor(code) {
555
+ super(code);
556
+ this.code = code;
557
+ this.name = "BrowserError";
558
+ }
559
+ }
560
+ function safeError(error) {
561
+ return error instanceof BrowserError ? error.code : "CONTACTS_UNAVAILABLE";
562
+ }
563
+
564
+ class NativeProtocol {
565
+ handlers;
566
+ buffer = Buffer.alloc(0);
567
+ queued = 0;
568
+ chain = Promise.resolve();
569
+ constructor(handlers) {
570
+ this.handlers = handlers;
571
+ }
572
+ accept(chunk) {
573
+ this.buffer = Buffer.concat([this.buffer, chunk]);
574
+ if (this.buffer.length > 1048576)
575
+ return this.handlers.fail();
576
+ while (this.buffer.length >= 4) {
577
+ const length = this.buffer.readUInt32LE(0);
578
+ if (!length || length > 65536)
579
+ return this.handlers.fail();
580
+ if (this.buffer.length < 4 + length)
581
+ break;
582
+ let message;
583
+ try {
584
+ message = JSON.parse(this.buffer.subarray(4, 4 + length).toString());
585
+ } catch {
586
+ return this.handlers.fail();
587
+ }
588
+ this.buffer = this.buffer.subarray(4 + length);
589
+ if (!message || typeof message.id !== "string" || !/^[0-9a-f-]{36}$/.test(message.id))
590
+ return this.handlers.fail();
591
+ if (++this.queued > 32)
592
+ return this.handlers.fail();
593
+ this.chain = this.chain.then(async () => {
594
+ try {
595
+ const result = await this.handlers.handle(message);
596
+ this.handlers.send({ id: message.id, ok: true, result });
597
+ } catch (e) {
598
+ this.handlers.send({ id: message.id, ok: false, error: safeError(e) });
599
+ } finally {
600
+ this.queued--;
601
+ }
602
+ });
603
+ }
604
+ }
605
+ async idle() {
606
+ await this.chain;
607
+ }
608
+ }
609
+ function encodeFrame(value) {
610
+ const body = Buffer.from(JSON.stringify(value));
611
+ if (body.length > 512000)
612
+ throw new BrowserError("RESPONSE_TOO_LARGE");
613
+ const header = Buffer.alloc(4);
614
+ header.writeUInt32LE(body.length);
615
+ return Buffer.concat([header, body]);
616
+ }
617
+
618
+ // src/browser/values.ts
619
+ function rec(value) {
620
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
621
+ }
622
+ function text(value, max = 200) {
623
+ return typeof value === "string" && value.length <= max ? value.trim() : "";
624
+ }
625
+ function list(value) {
626
+ return Array.isArray(value) ? value.map(rec).filter((x) => x !== null) : [];
627
+ }
628
+ function primary(items) {
629
+ return items.find((x) => x.is_primary === true) ?? items[0] ?? null;
630
+ }
631
+ function contactId(value) {
632
+ if (typeof value !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(value))
633
+ throw new BrowserError("CONTACT_NOT_FOUND");
634
+ return value;
635
+ }
636
+ function searchQuery(value) {
637
+ if (value === undefined || value === null)
638
+ return "";
639
+ if (typeof value !== "string")
640
+ throw new BrowserError("INVALID_QUERY");
641
+ return value.trim().slice(0, 200);
642
+ }
643
+ var REGION_NAMES = (() => {
644
+ try {
645
+ return new Intl.DisplayNames(["en"], { type: "region" });
646
+ } catch {
647
+ return null;
648
+ }
649
+ })();
650
+ var REGION_CODES = "AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW".split(" ");
651
+ var ALIAS_NAMES = {
652
+ usa: "US",
653
+ unitedstates: "US",
654
+ unitedstatesofamerica: "US",
655
+ uk: "GB",
656
+ unitedkingdom: "GB",
657
+ greatbritain: "GB",
658
+ england: "GB",
659
+ romania: "RO",
660
+ germany: "DE",
661
+ france: "FR",
662
+ spain: "ES",
663
+ italy: "IT",
664
+ netherlands: "NL",
665
+ thenetherlands: "NL"
666
+ };
667
+ function countryCode(value) {
668
+ const raw = value.trim();
669
+ if (!raw)
670
+ return "";
671
+ if (/^[A-Za-z]{2}$/.test(raw))
672
+ return raw.toUpperCase();
673
+ const norm = raw.toLowerCase().replace(/[^a-z]/g, "");
674
+ if (ALIAS_NAMES[norm])
675
+ return ALIAS_NAMES[norm];
676
+ if (REGION_NAMES) {
677
+ for (const code of REGION_CODES) {
678
+ let name;
679
+ try {
680
+ name = REGION_NAMES.of(code);
681
+ } catch {
682
+ name = undefined;
683
+ }
684
+ if (name && name.toLowerCase().replace(/[^a-z]/g, "") === norm)
685
+ return code;
686
+ }
687
+ }
688
+ return raw;
689
+ }
690
+ function contactSummary(contact) {
691
+ const c = rec(contact);
692
+ if (!c || typeof c.id !== "string")
693
+ return null;
694
+ const email = primary(list(c.emails));
695
+ const company = rec(c.company);
696
+ const name = text(c.display_name) || [text(c.first_name), text(c.last_name)].filter(Boolean).join(" ") || text(c.nickname) || "Unnamed contact";
697
+ return { id: c.id, name, company: text(company?.name, 120), email: text(email?.address, 254) };
698
+ }
699
+ function contactValues(contact) {
700
+ const c = rec(contact);
701
+ if (!c)
702
+ throw new BrowserError("CONTACT_NOT_FOUND");
703
+ const out = {};
704
+ const set = (key, value, max = 200) => {
705
+ const v = text(value, max);
706
+ if (v)
707
+ out[key] = v;
708
+ };
709
+ const first = text(c.first_name);
710
+ const last = text(c.last_name);
711
+ set("given-name", first);
712
+ set("family-name", last);
713
+ set("name", text(c.display_name) || [first, last].filter(Boolean).join(" "));
714
+ set("nickname", c.nickname);
715
+ set("organization", rec(c.company)?.name, 120);
716
+ set("organization-title", c.job_title, 120);
717
+ set("url", c.website, 2048);
718
+ const email = primary(list(c.emails));
719
+ set("email", email?.address, 254);
720
+ const phone = primary(list(c.phones));
721
+ if (phone) {
722
+ const number = text(phone.number, 40);
723
+ const code = text(phone.country_code, 6);
724
+ if (number)
725
+ out.tel = code && !number.startsWith("+") ? `${code.startsWith("+") ? code : `+${code}`} ${number}`.trim() : number;
726
+ }
727
+ const address = primary(list(c.addresses));
728
+ if (address) {
729
+ const street = text(address.street, 300);
730
+ if (street) {
731
+ const lines = street.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
732
+ out["street-address"] = lines.join(`
733
+ `);
734
+ out["address-line1"] = lines[0] ?? street;
735
+ if (lines[1])
736
+ out["address-line2"] = lines.slice(1).join(", ");
737
+ }
738
+ set("address-level2", address.city, 120);
739
+ set("address-level1", address.state, 120);
740
+ set("postal-code", address.zip, 40);
741
+ const country = text(address.country, 120);
742
+ if (country)
743
+ out.country = countryCode(country);
744
+ }
745
+ const birthday = text(c.birthday, 40);
746
+ if (/^\d{4}-\d{2}-\d{2}/.test(birthday))
747
+ out.bday = birthday.slice(0, 10);
748
+ return out;
749
+ }
750
+
751
+ // src/browser/native.ts
752
+ var version = (() => {
753
+ try {
754
+ return createRequire(import.meta.url)("../../package.json").version;
755
+ } catch {
756
+ return "unknown";
757
+ }
758
+ })();
759
+ if (process.argv[2] !== `chrome-extension://${EXTENSION_ID}/` || process.stdout.isTTY)
760
+ process.exit(1);
761
+ var client = null;
762
+ var configured = null;
763
+ function api() {
764
+ if (client)
765
+ return client;
766
+ if (configured)
767
+ throw new BrowserError(configured);
768
+ try {
769
+ client = createContactsClient();
770
+ return client;
771
+ } catch (error) {
772
+ configured = error instanceof ContactsClientConfigurationError ? "CONTACTS_NOT_CONFIGURED" : "CONTACTS_UNAVAILABLE";
773
+ throw new BrowserError(configured);
774
+ }
775
+ }
776
+ var browserId;
777
+ function send(value) {
778
+ process.stdout.write(encodeFrame(value));
779
+ }
780
+ process.stdin.on("end", () => process.exit(0));
781
+ process.stdin.on("error", () => process.exit(1));
782
+ process.stdout.on("error", () => process.exit(1));
783
+ async function handle(message) {
784
+ if (message.op === "hello") {
785
+ if (typeof message.browserId !== "string" || !/^[0-9a-f-]{36}$/.test(message.browserId))
786
+ throw new BrowserError("INVALID_BROWSER_ID");
787
+ browserId = message.browserId;
788
+ return { browserId, version };
789
+ }
790
+ if (!browserId)
791
+ throw new BrowserError("HELLO_REQUIRED");
792
+ if (message.op === "status")
793
+ return { browserId, version, configured: configured === null };
794
+ if (message.op === "search") {
795
+ const query = searchQuery(message.query);
796
+ const response = query ? await api().listContacts({ q: query, limit: 20 }) : await api().listContacts({ limit: 20, ...{ order_by: "updated_at", order_dir: "desc" } });
797
+ const contacts = Array.isArray(response?.contacts) ? response.contacts : [];
798
+ return contacts.map(contactSummary).filter((x) => x !== null);
799
+ }
800
+ if (message.op === "resolve") {
801
+ const id = contactId(message.contactId);
802
+ let response = null;
803
+ try {
804
+ response = await api().getContact(id);
805
+ } catch (error) {
806
+ if (error instanceof BrowserError)
807
+ throw error;
808
+ if (error?.status === 404)
809
+ throw new BrowserError("CONTACT_NOT_FOUND");
810
+ throw new BrowserError("CONTACTS_UNAVAILABLE");
811
+ }
812
+ const contact = response?.contact ?? response;
813
+ if (!contact || contact.id !== id)
814
+ throw new BrowserError("CONTACT_NOT_FOUND");
815
+ return contactValues(contact);
816
+ }
817
+ throw new BrowserError("UNKNOWN_OPERATION");
818
+ }
819
+ var protocol = new NativeProtocol({ handle, send, fail: () => process.exit(1) });
820
+ process.stdin.on("data", (chunk) => {
821
+ protocol.accept(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
822
+ });