@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.
- package/index.mjs +59 -10
- 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
|
-
|
|
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 &&
|
|
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(
|
|
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,
|
|
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}`,
|
|
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
|
-
|
|
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);
|