@xuda.io/account_module 1.2.2310 → 1.2.2311

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 (2) hide show
  1. package/index.mjs +81 -130
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -3535,6 +3535,7 @@ export const get_account_name = async function (req) {
3535
3535
  active_account_profile_id,
3536
3536
  network_country_code,
3537
3537
  public_profile_disabled,
3538
+ verify_level,
3538
3539
  } = data.data.account_info;
3539
3540
 
3540
3541
  obj = {
@@ -3570,6 +3571,11 @@ export const get_account_name = async function (req) {
3570
3571
  // email,// email?.[0] || '',
3571
3572
  avatar_source,
3572
3573
  active_account_profile_id,
3574
+ // The verification level of the account behind a card. Every caller that
3575
+ // builds a contact card off another account copies this onto the card doc
3576
+ // so the background tile can say what the level is; without it a card for
3577
+ // a verified person read as L0, which is a false statement about them.
3578
+ verify_level,
3573
3579
  };
3574
3580
  }
3575
3581
  return { code: 1, data: obj }; //is_online: data?.data?.socket_id ? true : false
@@ -4305,6 +4311,13 @@ const level_pattern = function (doc) {
4305
4311
  return `level-${level}-pattern.png`;
4306
4312
  };
4307
4313
 
4314
+ // The level tile is the DEFAULT for every contact card, not a fallback. It used
4315
+ // to be computed first and then overwritten by a tile keyed off where the
4316
+ // contact came from (xu / authentic / fictional / email / web), so a card for a
4317
+ // verified person showed the generic XU tile and the level never appeared on any
4318
+ // card that had a contact_uid, which is every card for a real account. Only the
4319
+ // states that say the card cannot show a level at all still win: a shared card
4320
+ // wears its owner's avatar, an avatar being processed says so, and spam says so.
4308
4321
  const get_contact_pattern = async function (doc) {
4309
4322
  let ret = level_pattern(doc);
4310
4323
 
@@ -4325,84 +4338,12 @@ const get_contact_pattern = async function (doc) {
4325
4338
  }
4326
4339
  } else if (doc.is_spam) {
4327
4340
  ret = `spam-pattern.png`;
4328
- } else {
4329
- // if (doc.account_type === 'business') {
4330
- // // if (doc?.is_real_person) {
4331
- // ret = doc.profile_picture;
4332
- // // }
4333
- // } else {
4334
- // if (doc.my_contact) {
4335
- // if (doc.account_type === 'business') {
4336
- // ret = doc.profile_picture;
4337
- // } else {
4338
- // switch (doc.avatar_source) {
4339
- // case 'fictional':
4340
- // ret = `fictional-avatar-pattern.png`;
4341
- // break;
4342
-
4343
- // case 'ai profile':
4344
- // ret = `fictional-avatar-pattern.png`;
4345
- // break;
4346
-
4347
- // case 'authentic profile':
4348
- // ret = `authentic-avatar-pattern.png`;
4349
- // break;
4350
-
4351
- // default:
4352
- // ret = `xu-pattern.png`;
4353
- // break;
4354
- // }
4355
- // }
4356
- // } else
4357
-
4358
- // if (doc?.shared_from_uid) {
4359
- // //&& (doc.pending || (doc.team_req_id && doc.contact_share_copy))) {
4360
- // // pending or active share request
4361
- // const shared_from_uid_ret = await get_account_name({ uid_query: doc.shared_from_uid });
4362
- // ret = shared_from_uid_ret.data.profile_avatar;
4363
- // }
4364
-
4365
- // else if (doc?.reference_doc?.shared_from_uid) {
4366
- // //active share
4367
- // const shared_from_uid_ret = await get_account_name({ uid_query: doc.reference_doc.shared_from_uid });
4368
- // ret = shared_from_uid_ret.data.profile_avatar;
4369
- // }
4370
- // else
4371
- if (doc.contact_uid) {
4372
- switch (doc.avatar_source) {
4373
- case 'fictional':
4374
- ret = `fictional-avatar-pattern.png`;
4375
- break;
4376
-
4377
- case 'ai profile':
4378
- ret = `fictional-avatar-pattern.png`;
4379
- break;
4380
-
4381
- case 'authentic profile':
4382
- ret = `authentic-avatar-pattern.png`;
4383
- break;
4384
-
4385
- default:
4386
- ret = `xu-pattern.png`;
4387
- break;
4388
- }
4389
- } else {
4390
- switch (doc.source) {
4391
- case 'read emails': {
4392
- ret = `email-pattern.png`;
4393
- break;
4394
- }
4395
- case 'web': {
4396
- ret = `web-pattern.png`;
4397
- break;
4398
- }
4399
-
4400
- default:
4401
- break;
4402
- }
4403
- }
4404
- // }
4405
4341
  }
4342
+ // Everything else keeps the level tile. The old where-it-came-from tiles
4343
+ // (xu / authentic / fictional for an account, email / web for a scraped
4344
+ // contact) are gone on purpose: they occupied the one surface that now
4345
+ // carries the verification level, and a contact with no account behind it
4346
+ // has no level to show, which is exactly what L0 says.
4406
4347
  return ret;
4407
4348
  };
4408
4349
 
@@ -4570,6 +4511,10 @@ export const get_contact_info = async function (uid, contact_doc, _id) {
4570
4511
  }
4571
4512
 
4572
4513
  doc.avatar_source = account_info_ret.data.avatar_source;
4514
+ // The card's background tile is the contact's verification level, and the
4515
+ // level lives on the account, not on my copy of them. Carry it over here,
4516
+ // where the account is already in hand, so the tile costs no extra read.
4517
+ doc.verify_level = account_info_ret.data.verify_level;
4573
4518
  // doc.icon_pattern = await get_contact_pattern(doc);
4574
4519
  doc.username = account_info_ret.data.username;
4575
4520
  doc.business_name = account_info_ret.data.business_name;
@@ -4634,6 +4579,7 @@ export const get_pending_contact_out = async function (uid, _id) {
4634
4579
  doc.account_type = account_info_ret.data.account_type;
4635
4580
  doc.avatar_source = account_info_ret.data.avatar_source;
4636
4581
  doc.profile_avatar = account_info_ret.data.profile_avatar;
4582
+ doc.verify_level = account_info_ret.data.verify_level;
4637
4583
  doc.name = `${account_info_ret.data.first_name} ${account_info_ret.data.last_name}`;
4638
4584
  }
4639
4585
  doc.icon_pattern = await get_contact_pattern(doc);
@@ -4665,6 +4611,7 @@ export const get_pending_contact_in = async function (uid, _id) {
4665
4611
  doc.account_type = account_info_ret.data.account_type;
4666
4612
  doc.avatar_source = account_info_ret.data.avatar_source;
4667
4613
  doc.profile_avatar = account_info_ret.data.profile_avatar;
4614
+ doc.verify_level = account_info_ret.data.verify_level;
4668
4615
  doc.name = `${account_info_ret.data.first_name} ${account_info_ret.data.last_name}`;
4669
4616
  }
4670
4617
  doc.card_background = get_contact_background({ pending: true });
@@ -4704,6 +4651,7 @@ const get_user_contact = async function (uid, uid_query) {
4704
4651
  doc.profile_avatar = account_info.profile_avatar;
4705
4652
  doc.profile_picture = account_info.profile_picture;
4706
4653
  doc.avatar_source = account_info.avatar_source;
4654
+ doc.verify_level = account_info.verify_level;
4707
4655
  doc.card_background = get_contact_background(doc);
4708
4656
  doc.icon_pattern = await get_contact_pattern(doc);
4709
4657
  doc.border = get_contact_border(doc);
@@ -5618,28 +5566,18 @@ setTimeout(async () => {
5618
5566
 
5619
5567
  //////// CONTACTS //////////////
5620
5568
 
5621
- function identifyPersonalProvider(domain) {
5622
- const providers = {
5623
- 'gmail.com': 'Google Gmail',
5624
- 'googlemail.com': 'Google Gmail',
5625
- 'outlook.com': 'Microsoft Outlook',
5626
- 'hotmail.com': 'Microsoft Hotmail',
5627
- 'live.com': 'Microsoft Live',
5628
- 'yahoo.com': 'Yahoo Mail',
5629
- 'ymail.com': 'Yahoo Mail',
5630
- 'aol.com': 'AOL Mail',
5631
- 'icloud.com': 'Apple iCloud',
5632
- 'me.com': 'Apple iCloud',
5633
- 'mac.com': 'Apple iCloud',
5634
- 'protonmail.com': 'ProtonMail',
5635
- 'proton.me': 'ProtonMail',
5636
- 'zoho.com': 'Zoho Mail',
5637
- 'yandex.com': 'Yandex Mail',
5638
- 'mail.com': 'Mail.com',
5639
- 'gmx.com': 'GMX Mail',
5640
- };
5569
+ // A "business" that resolves to the address's own mail provider is not the contact's
5570
+ // employer, it is Gmail. get_business_info, handed a bare gmail.com address, answers
5571
+ // "Gmail / gmail.com / Technology" rather than "Not available", so the only escape hatch the
5572
+ // classification had (business_name === 'Not available') never fired and every personal
5573
+ // Gmail contact came out as a business, storefront placeholder and all. This is the second
5574
+ // escape hatch, built on the provider list that already exists directly below.
5575
+ const businessIsJustTheMailProvider = (business_info) => !!identifyPersonalProvider(business_info?.business_domain || '');
5641
5576
 
5642
- return providers[domain.toLowerCase()];
5577
+ function identifyPersonalProvider(domain) {
5578
+ // The list moved to common so ai_module's contact classifier reads the same one. It used to
5579
+ // live here alone, which is exactly why the classifier could call gmail.com a business.
5580
+ return _common.personal_email_provider(domain);
5643
5581
  }
5644
5582
 
5645
5583
  export const add_contact = async function (req, job_id, headers) {
@@ -5714,21 +5652,24 @@ export const add_contact = async function (req, job_id, headers) {
5714
5652
  await activity('spam_check', { by: 'ai', is_spam: !!is_spam, subject: metadata.subject });
5715
5653
  }
5716
5654
  if (!is_spam) {
5717
- const is_business = await ai_ms.is_business_contact(uid, email, name, metadata.subject, metadata.summarized_body, account_profile_info);
5655
+ const verdict = await ai_ms.is_business_contact(uid, email, name, metadata.subject, metadata.summarized_body, account_profile_info);
5656
+ // Tolerates the old bare boolean, in case a stale ai_module answers this call.
5657
+ const is_business = typeof verdict === 'object' && verdict !== null ? verdict.is_business : !!verdict;
5718
5658
  account_type = is_business ? 'business' : 'personal';
5719
- await activity('classified', { by: 'ai', account_type, inputs: { email: email.toLowerCase(), name: name || '', subject: metadata.subject || '' } });
5659
+ await activity('classified', { by: 'ai', account_type, confidence: verdict?.confidence, why: verdict?.reason, inputs: { email: email.toLowerCase(), name: name || '', subject: metadata.subject || '' } });
5720
5660
  if (is_business) {
5721
5661
  business_info = await ai_ms.get_business_info(uid, '', email, account_profile_info, { light: true });
5722
- if (business_info.business_name !== 'Not available') {
5662
+ if (business_info.business_name !== 'Not available' && !businessIsJustTheMailProvider(business_info)) {
5723
5663
  await activity('business_info', { by: 'ai', depth: 'light', business_name: business_info.business_name, business_domain: business_info.business_domain, business_category: business_info.business_category });
5724
5664
  business_has_person = await ai_ms.is_business_contact_has_person(uid, email, name, metadata.subject, metadata.summarized_body, account_profile_info);
5725
5665
  await activity('business_has_person', { by: 'ai', has_person: !!business_has_person });
5726
5666
  } else {
5667
+ const provider = identifyPersonalProvider(business_info?.business_domain || '');
5727
5668
  account_type = 'personal';
5728
5669
  business_info = undefined;
5729
5670
  // The reversal matters more than the original verdict: the card says
5730
5671
  // personal even though the business call answered yes.
5731
- await activity('classified', { by: 'ai', account_type, reverted_from: 'business', reason: 'no business could be identified behind the address' });
5672
+ await activity('classified', { by: 'ai', account_type, reverted_from: 'business', reason: provider ? `the only business behind the address is its mail provider (${provider})` : 'no business could be identified behind the address' });
5732
5673
  }
5733
5674
  }
5734
5675
  if (_.isEmpty(cached_contact) && account_type === 'business' && !business_has_person) {
@@ -6372,19 +6313,31 @@ export const not_spam_contact = async function (req, job_id, headers) {
6372
6313
  }
6373
6314
 
6374
6315
  const previous_account_type = contact_doc.account_type;
6375
- const is_business = await ai_ms.is_business_contact(uid, contact_doc.email, contact_doc.name, contact_doc?.metadata?.subject, contact_doc?.metadata?.summarized_body, account_profile_info);
6316
+ const verdict = await ai_ms.is_business_contact(uid, contact_doc.email, contact_doc.name, contact_doc?.metadata?.subject, contact_doc?.metadata?.summarized_body, account_profile_info);
6317
+ const is_business = typeof verdict === 'object' && verdict !== null ? verdict.is_business : !!verdict;
6376
6318
  contact_doc.account_type = is_business ? 'business' : 'personal';
6377
- await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', trigger: 'not spam', account_type: contact_doc.account_type, previous_account_type: previous_account_type || 'unset' });
6319
+ await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', trigger: 'not spam', account_type: contact_doc.account_type, confidence: verdict?.confidence, why: verdict?.reason, previous_account_type: previous_account_type || 'unset' });
6320
+ if (!is_business && (contact_doc.business_info || contact_doc.business_has_person)) {
6321
+ // A contact that reclassifies from business to person used to keep the company profile
6322
+ // the old verdict attached to it, because business_info is only ever written inside the
6323
+ // is_business branch and nothing clears it on the way out. The card then showed a person
6324
+ // with a company still on file.
6325
+ const dropped = contact_doc?.business_info?.business_name;
6326
+ contact_doc.business_info = undefined;
6327
+ contact_doc.business_has_person = undefined;
6328
+ await log_contact_activity(uid, contact_id, 'business_info_cleared', { reason: 'contact reclassified as a person', dropped });
6329
+ }
6378
6330
  if (is_business) {
6379
6331
  contact_doc.business_info = await ai_ms.get_business_info(uid, '', contact_doc.email, account_profile_info, { light: true });
6380
- if (contact_doc.business_info.business_name !== 'Not available') {
6332
+ if (contact_doc.business_info.business_name !== 'Not available' && !businessIsJustTheMailProvider(contact_doc.business_info)) {
6381
6333
  await log_contact_activity(uid, contact_id, 'business_info', { by: 'ai', depth: 'light', business_name: contact_doc.business_info.business_name, business_domain: contact_doc.business_info.business_domain, business_category: contact_doc.business_info.business_category });
6382
6334
  contact_doc.business_has_person = await ai_ms.is_business_contact_has_person(uid, contact_doc.email, contact_doc.name, contact_doc?.metadata?.subject, contact_doc?.metadata?.summarized_body, account_profile_info);
6383
6335
  await log_contact_activity(uid, contact_id, 'business_has_person', { by: 'ai', has_person: !!contact_doc.business_has_person });
6384
6336
  } else {
6337
+ const provider = identifyPersonalProvider(contact_doc.business_info?.business_domain || '');
6385
6338
  contact_doc.account_type = 'personal';
6386
6339
  contact_doc.business_info = undefined;
6387
- await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', account_type: 'personal', reverted_from: 'business', reason: 'no business could be identified behind the address' });
6340
+ await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', account_type: 'personal', reverted_from: 'business', reason: provider ? `the only business behind the address is its mail provider (${provider})` : 'no business could be identified behind the address' });
6388
6341
  }
6389
6342
  }
6390
6343
 
@@ -6471,14 +6424,15 @@ export const set_deep_research_contact = async function (req, job_id, headers) {
6471
6424
 
6472
6425
  if (contact_doc.account_type === 'business') {
6473
6426
  contact_doc.business_info = await ai_ms.get_business_info(uid, '', contact_doc.email, account_profile_info);
6474
- if (contact_doc.business_info.business_name !== 'Not available') {
6427
+ if (contact_doc.business_info.business_name !== 'Not available' && !businessIsJustTheMailProvider(contact_doc.business_info)) {
6475
6428
  await log_contact_activity(uid, contact_id, 'business_info', { by: 'ai', depth: 'deep', business_name: contact_doc.business_info.business_name, business_domain: contact_doc.business_info.business_domain, business_category: contact_doc.business_info.business_category });
6476
6429
  contact_doc.business_has_person = await ai_ms.is_business_contact_has_person(uid, contact_doc.email, contact_doc.name, contact_doc?.metadata?.subject, contact_doc?.metadata?.summarized_body, account_profile_info);
6477
6430
  await log_contact_activity(uid, contact_id, 'business_has_person', { by: 'ai', has_person: !!contact_doc.business_has_person });
6478
6431
  } else {
6432
+ const provider = identifyPersonalProvider(contact_doc.business_info?.business_domain || '');
6479
6433
  contact_doc.account_type = 'personal';
6480
6434
  contact_doc.business_info = undefined;
6481
- await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', account_type: 'personal', reverted_from: 'business', reason: 'deep research found no business behind the address' });
6435
+ await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', account_type: 'personal', reverted_from: 'business', reason: provider ? `the only business behind the address is its mail provider (${provider})` : 'deep research found no business behind the address' });
6482
6436
  }
6483
6437
  }
6484
6438
 
@@ -6568,7 +6522,16 @@ export const get_contact_activity = async function (req) {
6568
6522
  try {
6569
6523
  if (!contact_id) throw new Error('contact_id is missing');
6570
6524
  const account_profile_info = await get_active_account_profile_info(uid);
6571
- const contact_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, contact_id);
6525
+ // get_app_couch_doc_native THROWS a bare couch 'missing' for an id that is not in
6526
+ // this account's app db (an id from another account is the common way to get here),
6527
+ // so the not-found check below never ran and the caller saw "missing" with nothing
6528
+ // to act on. Catch it and answer the question that was actually asked.
6529
+ let contact_doc;
6530
+ try {
6531
+ contact_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, contact_id);
6532
+ } catch (_) {
6533
+ contact_doc = null;
6534
+ }
6572
6535
  if (!contact_doc || contact_doc.docType !== 'contact') throw new Error(`contact ${contact_id} not found`);
6573
6536
  if (contact_doc.uid !== uid && contact_doc?.uid_created !== uid) throw new Error('Operation not allowed');
6574
6537
 
@@ -6688,7 +6651,11 @@ export const get_account_profile_info = async function (uid, contact_profile_doc
6688
6651
 
6689
6652
  const get_pattern = async function (doc) {
6690
6653
  // Same level-based default as get_contact_pattern, so a card shows the same
6691
- // verification tile whichever path built it.
6654
+ // verification tile whichever path built it, and the same two states that
6655
+ // outrank a level: a shared profile wears its owner's avatar, spam says
6656
+ // spam. A business profile used to paint its own logo across the card face,
6657
+ // which is why a verified business showed no level at all; the logo is
6658
+ // already the avatar on the same card, so the tile is the level here too.
6692
6659
  let ret = level_pattern(doc);
6693
6660
 
6694
6661
  if (doc.shared_from_uid) {
@@ -6697,26 +6664,6 @@ export const get_account_profile_info = async function (uid, contact_profile_doc
6697
6664
  ret = shared_from_uid_ret.data.profile_avatar;
6698
6665
  } else if (doc.is_spam) {
6699
6666
  ret = `spam-pattern.png`;
6700
- } else if (doc.account_type === 'business') {
6701
- ret = doc.profile_picture;
6702
- } else {
6703
- switch (doc.avatar_source) {
6704
- case 'fictional':
6705
- ret = `fictional-avatar-pattern.png`;
6706
- break;
6707
-
6708
- case 'ai profile':
6709
- ret = `fictional-avatar-pattern.png`;
6710
- break;
6711
-
6712
- case 'authentic profile':
6713
- ret = `authentic-avatar-pattern.png`;
6714
- break;
6715
-
6716
- default:
6717
- ret = `xu-pattern.png`;
6718
- break;
6719
- }
6720
6667
  }
6721
6668
 
6722
6669
  return ret;
@@ -6774,6 +6721,10 @@ export const get_account_profile_info = async function (uid, contact_profile_doc
6774
6721
  // console.log(stringToColour('Project Z')); // -> Consistent color for "Project Z"
6775
6722
 
6776
6723
  doc.avatar_source = account_info_ret.data.avatar_source;
6724
+ // A profile inherits the verification level of the account that owns it (a
6725
+ // shared profile, the account that shared it), so its card reads the same
6726
+ // level as its owner's contact card instead of falling back to L0.
6727
+ doc.verify_level = account_info_ret.data.verify_level;
6777
6728
  doc.icon_pattern = await get_pattern(doc);
6778
6729
  doc.name = doc.profile_name;
6779
6730
  doc.interactions = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/account_module",
3
- "version": "1.2.2310",
3
+ "version": "1.2.2311",
4
4
  "description": "Xuda Account Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {