@xuda.io/account_module 1.2.2291 → 1.2.2292

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 +59 -10
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -2287,7 +2287,10 @@ export const ops_deploy_server = async function (req = {}) {
2287
2287
  return { code: -1, data: `enqueue write to orchestrator couch failed: ${e.message}` };
2288
2288
  }
2289
2289
 
2290
- _ops_notify_ops(`Server deploy queued (${mode}): ${server_id}`, [['Server', server_id], ['Mode', mode], ['By', req.uid], ['State', 'queued'], ['Orchestrator', 'dev.xuda.ai']], 'OPS-DEPLOY-' + server_id);
2290
+ // No ops notification for the 'queued' state: it is immediate feedback on a button
2291
+ // the operator just clicked (the queued state is returned to the dashboard below and
2292
+ // pollable via ops_server_deploy_status), so the email/banner was pure noise. The
2293
+ // audit log is kept for traceability. Deploy done/failed notifications are unaffected.
2291
2294
  logs_msa.add_account_log({ uid: req.uid, method: 'ops_deploy_server', source: 'ops dashboard', response_status_code: 200, response_data: `queued ${mode} deploy for ${server_id} by ${req.uid}` });
2292
2295
 
2293
2296
  return { code: 1, data: { enqueued: true, server_id, mode, state: 'queued', orchestrator: 'dev.xuda.ai', poll: 'ops_server_deploy_status' } };
@@ -2789,6 +2792,34 @@ export const get_active_account_profile_info = async function (uid, profile_id)
2789
2792
  // (e.g. interrupted Google signup flow), instead of throwing on every chat-widget poll.
2790
2793
  let active_account_profile_id = profile_id || acc_obj.account_info?.active_account_profile_id || acc_obj.account_profile_id;
2791
2794
 
2795
+ if (!active_account_profile_id) {
2796
+ // Lazy self-heal: an interrupted signup can leave an orphan account_profile doc in the
2797
+ // project db while the account itself was never linked (account_profile_id unset). Rather
2798
+ // than return null forever, adopt an existing profile here, on the very read path the chat
2799
+ // widget polls, and persist the link so it is a one-time repair. Gated by the same dedup set
2800
+ // as the warning below, so a truly profile-less account is queried at most once per process.
2801
+ if (!_warned_no_profile_id.has(acc_obj._id)) {
2802
+ try {
2803
+ const existing = await db_module.find_app_couch_query(acc_obj.account_project_id, {
2804
+ selector: { docType: 'account_profile', uid },
2805
+ limit: 50,
2806
+ });
2807
+ const docs = (existing && existing.docs) || [];
2808
+ if (docs.length) {
2809
+ const chosen = docs.find((d) => d.main) || docs.slice().sort((a, b) => (b.date_created_ts || 0) - (a.date_created_ts || 0))[0];
2810
+ active_account_profile_id = chosen._id;
2811
+ acc_obj.account_profile_id = chosen._id;
2812
+ acc_obj.account_info = acc_obj.account_info || {};
2813
+ acc_obj.account_info.active_account_profile_id = chosen._id;
2814
+ await db_module.save_couch_doc('xuda_accounts', acc_obj); // conflict-safe (retry loop)
2815
+ console.log(`[get_active_account_profile_info] self-healed acc ${acc_obj._id}: adopted orphan profile ${chosen._id}`);
2816
+ }
2817
+ } catch (heal_err) {
2818
+ /* fall through to the soft-fail below on any heal error */
2819
+ }
2820
+ }
2821
+ }
2822
+
2792
2823
  if (!active_account_profile_id) {
2793
2824
  // Soft-fail: log once PER ACCOUNT and return a degraded response. Callers that need a real
2794
2825
  // profile should null-check `account_profile_obj`. The chat widget treats this as an anonymous
@@ -6644,12 +6675,20 @@ export const evaluate_credit_gate = async function (req) {
6644
6675
  return { block: over, scope: over ? 'pool' : null, reason: over ? _SCOPE_REASON.ledger : '', hard_breached: over ? [{ scope: 'pool', key: 'ledger' }] : [], soft_breached: [], account_id: owner };
6645
6676
  }
6646
6677
 
6678
+ // The MAIN profile is not sharable, so per-profile credit control has no
6679
+ // meaning on it: there is no member to scope it to, and the pool cap already
6680
+ // covers the owner. Ignore any rule stored against it, including legacy docs
6681
+ // written before the editor stopped offering it, so it can never gate the
6682
+ // owner's own profile.
6683
+ const main_apid = account_doc?.account_profile_id;
6684
+ const profile_rule = apid && apid !== main_apid ? rules.profiles?.[apid] : null;
6685
+
6647
6686
  // Freeze (hard stop, no usage lookup needed).
6648
- if (apid && rules.profiles?.[apid]?.frozen) return _block(owner, 'freeze', 'This profile is paused.', { scope: 'profile', key: apid, kind: 'frozen' });
6687
+ if (apid && profile_rule?.frozen) return _block(owner, 'freeze', 'This profile is paused.', { scope: 'profile', key: apid, kind: 'frozen' });
6649
6688
  if (member_uid && rules.users?.[member_uid]?.frozen) return _block(owner, 'freeze', 'This member is paused.', { scope: 'user', key: member_uid, kind: 'frozen' });
6650
6689
 
6651
6690
  // Model access (allow/deny): profile rule then user rule.
6652
- if (_model_access_blocks(rules.profiles?.[apid]?.model_access, model_code)) return _block(owner, 'model_access', 'This model is not allowed for this profile.', { scope: 'model', key: model_code, kind: 'model_access' });
6691
+ if (_model_access_blocks(profile_rule?.model_access, model_code)) return _block(owner, 'model_access', 'This model is not allowed for this profile.', { scope: 'model', key: model_code, kind: 'model_access' });
6653
6692
  if (_model_access_blocks(rules.users?.[member_uid]?.model_access, model_code)) return _block(owner, 'model_access', 'This model is not allowed for this member.', { scope: 'model', key: model_code, kind: 'model_access' });
6654
6693
 
6655
6694
  // Usage-based caps.
@@ -6670,12 +6709,12 @@ export const evaluate_credit_gate = async function (req) {
6670
6709
  // Pool: ledger safety (remaining below zero) plus the configured pool guardrail.
6671
6710
  if (su.credits_total - su.total < -0.5) hard.push({ scope: 'pool', key: 'ledger' });
6672
6711
  consider('pool', 'pool', rules.pool, su.total);
6673
- if (apid) consider('profile', apid, rules.profiles?.[apid], su.by_profile[apid] || 0);
6712
+ if (apid) consider('profile', apid, profile_rule, su.by_profile[apid] || 0);
6674
6713
  if (member_uid) consider('user', member_uid, rules.users?.[member_uid], su.by_user[member_uid] || 0);
6675
6714
  if (model_code) consider('model', model_code, rules.models?.[model_code], su.by_model[model_code] || 0);
6676
6715
  if (source) {
6677
6716
  consider('source', source, rules.sources?.[source], su.by_source[source] || 0);
6678
- if (apid) consider('source', `${apid}:${source}`, rules.profiles?.[apid]?.sources?.[source], su.by_source[source] || 0);
6717
+ if (apid) consider('source', `${apid}:${source}`, profile_rule?.sources?.[source], su.by_source[source] || 0);
6679
6718
  }
6680
6719
 
6681
6720
  if (hard.length) {
@@ -6694,11 +6733,17 @@ export const evaluate_credit_gate = async function (req) {
6694
6733
  };
6695
6734
 
6696
6735
  // Originals only (not shared copies) belonging to the owner.
6697
- const _list_owner_profiles = async function (owner_uid, app_id) {
6736
+ const _list_owner_profiles = async function (owner_uid, app_id, main_profile_id) {
6698
6737
  if (!app_id) return [];
6699
6738
  try {
6700
6739
  const ret = await db_module.find_app_couch_query(app_id, { selector: { docType: 'account_profile', uid: owner_uid, stat: 3 }, fields: ['_id', 'profile_name', 'share_item_id'], limit: 500 });
6701
- return (ret?.docs || []).filter((d) => !d.share_item_id).map((d) => ({ account_profile_id: d._id, profile_name: d.profile_name || 'Profile' }));
6740
+ // Drop the MAIN profile: it is not sharable, so there is no member to scope
6741
+ // credit control to and the pool cap already covers the owner. Leaving it in
6742
+ // this list is what made the editor offer per-profile limits and model
6743
+ // restrictions that could only ever gate the owner themselves.
6744
+ return (ret?.docs || [])
6745
+ .filter((d) => !d.share_item_id && d._id !== main_profile_id)
6746
+ .map((d) => ({ account_profile_id: d._id, profile_name: d.profile_name || 'Profile' }));
6702
6747
  } catch (e) {
6703
6748
  console.error('[credit] list profiles', e?.message || e);
6704
6749
  return [];
@@ -6758,7 +6803,7 @@ export const get_credit_management = async function (req) {
6758
6803
  }
6759
6804
 
6760
6805
  const app_id = account_doc?.account_project_id;
6761
- const profiles = await _list_owner_profiles(uid, app_id);
6806
+ const profiles = await _list_owner_profiles(uid, app_id, account_doc?.account_profile_id);
6762
6807
  const members = await _list_owner_members(uid, profiles);
6763
6808
 
6764
6809
  return { code: 1, data: { rules, can_edit, plan, entitlements, usage_breakdown, model_catalog, profiles, members } };
@@ -6800,7 +6845,7 @@ const _normalize_sources = function (src) {
6800
6845
  }
6801
6846
  return out;
6802
6847
  };
6803
- const _normalize_credit_rules = function (input, uid) {
6848
+ const _normalize_credit_rules = function (input, uid, main_profile_id) {
6804
6849
  const d = _default_credit_rules();
6805
6850
  const r = input && typeof input === 'object' ? input : {};
6806
6851
  const out = {
@@ -6821,6 +6866,10 @@ const _normalize_credit_rules = function (input, uid) {
6821
6866
  };
6822
6867
  if (r.profiles && typeof r.profiles === 'object') {
6823
6868
  for (const [apid, pv] of Object.entries(r.profiles)) {
6869
+ // Never persist a rule for the MAIN profile: it is not sharable, so credit
6870
+ // control there could only gate the owner. Dropping it here also cleans up
6871
+ // any legacy entry the moment the rules are next saved.
6872
+ if (main_profile_id && apid === main_profile_id) continue;
6824
6873
  out.profiles[apid] = { ..._normalize_softhard(pv, true), frozen: !!pv?.frozen, model_access: _normalize_model_access(pv?.model_access), sources: _normalize_sources(pv?.sources) };
6825
6874
  }
6826
6875
  }
@@ -6851,7 +6900,7 @@ export const set_credit_rules = async function (req) {
6851
6900
  if (acc_ret.code < 0) return acc_ret;
6852
6901
  const account_doc = acc_ret.data;
6853
6902
  if (!_is_team_tier(account_doc?.membership_plan)) return { code: -1, data: 'Credit management is available on the Team plan.' };
6854
- account_doc.credit_rules = _normalize_credit_rules(req.rules, uid);
6903
+ account_doc.credit_rules = _normalize_credit_rules(req.rules, uid, account_doc?.account_profile_id);
6855
6904
  await db_module.save_couch_doc('xuda_accounts', account_doc);
6856
6905
  delete _scoped_usage_cache[uid];
6857
6906
  broadcast_credits(uid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/account_module",
3
- "version": "1.2.2291",
3
+ "version": "1.2.2292",
4
4
  "description": "Xuda Account Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {