@xuda.io/account_module 1.2.2285 → 1.2.2287
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 +338 -4
- package/index_ms.mjs +20 -0
- package/index_msa.mjs +20 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -57,6 +57,8 @@ const team_ms = await import(`${module_path}/team_module/index_ms.mjs`);
|
|
|
57
57
|
const email_ms = await import(`${module_path}/email_module/index_ms.mjs`);
|
|
58
58
|
const marketplace_ms = await import(`${module_path}/marketplace_module/index_ms.mjs`);
|
|
59
59
|
const stripe_ms = await import(`${module_path}/stripe_module/index_ms.mjs`);
|
|
60
|
+
// Revoking an SSH key from the servers it is installed on (disable/delete/expiry).
|
|
61
|
+
const deploy_ms = await import(`${module_path}/deploy_module/index_ms.mjs`);
|
|
60
62
|
// Awaitable app handle for the ops termination flow (power off / destroy).
|
|
61
63
|
const app_ms = await import(`${module_path}/app_module/index_ms.mjs`);
|
|
62
64
|
|
|
@@ -1957,6 +1959,177 @@ export const ops_send_notification = async function (req = {}) {
|
|
|
1957
1959
|
} catch (err) { return { code: -1, data: err.message }; }
|
|
1958
1960
|
};
|
|
1959
1961
|
|
|
1962
|
+
// ---------------------------------------------------------------------------
|
|
1963
|
+
// Fleet server admin (superuser) — list the platform's own servers, queue a
|
|
1964
|
+
// soft/hard code deploy, and poll deploy status/progress. These are thin, SAFE
|
|
1965
|
+
// wrappers over the existing, battle-tested publish engine in misc_module
|
|
1966
|
+
// (_publish_rollout / _reinstall_server_detached): a deploy here only sets
|
|
1967
|
+
// stat:1 + publish_mode on the target's xuda_servers doc — the dev
|
|
1968
|
+
// orchestrator's 1-min tick (controller_module -> misc.publish_to_servers)
|
|
1969
|
+
// picks it up and runs the drain -> reinstall -> undrain rollout, writing
|
|
1970
|
+
// progress back onto the SAME doc (stat 1->2->3/4, stat_ts, publish_error). So
|
|
1971
|
+
// the blast radius of THIS module is a single CouchDB field write; the vetted
|
|
1972
|
+
// engine does the actual work. Deploy targets are region app-servers only
|
|
1973
|
+
// (docType 'regional_server' + application_server); farm/proxmox nodes and the
|
|
1974
|
+
// dev orchestrator itself are never deployable here.
|
|
1975
|
+
// ---------------------------------------------------------------------------
|
|
1976
|
+
|
|
1977
|
+
// stat state-machine on an xuda_servers doc -> friendly deploy state.
|
|
1978
|
+
const _SRV_DEPLOY_STATE = { 1: 'queued', 2: 'deploying', 3: 'idle', 4: 'failed' };
|
|
1979
|
+
const _srv_deploy_state = (doc) => _SRV_DEPLOY_STATE[doc && doc.stat] || 'unknown';
|
|
1980
|
+
const _srv_busy = (doc) => !!doc && (doc.stat === 1 || doc.stat === 2);
|
|
1981
|
+
// A deploy target is a region control-plane app-server (runs the /var/xuda
|
|
1982
|
+
// stack). NOT farm/proxmox nodes (no stack) and NOT dev (the orchestrator
|
|
1983
|
+
// itself — it deploys via rsync, and can't reinstall itself mid-rollout).
|
|
1984
|
+
const _srv_is_deployable = (doc) => !!(doc && doc.docType === 'regional_server' && doc.application_server === true && doc._id !== 'dev');
|
|
1985
|
+
const _srv_role = (doc) => {
|
|
1986
|
+
if (!doc) return 'other';
|
|
1987
|
+
if (doc.docType === 'regional_server' && doc.application_server) return 'region';
|
|
1988
|
+
if (doc.provider === 'proxmox' && doc.vmid) return 'farm_vm';
|
|
1989
|
+
if (doc.provider === 'proxmox' && (doc.node || doc.api_url)) return 'farm_node';
|
|
1990
|
+
return 'other';
|
|
1991
|
+
};
|
|
1992
|
+
|
|
1993
|
+
// Load every xuda_servers doc via _all_docs (nano .list) rather than a
|
|
1994
|
+
// match-all Mango selector — avoids the "no matching index" warning that would
|
|
1995
|
+
// otherwise spam the log on every (frequently-polled) status call.
|
|
1996
|
+
const _load_all_server_docs = async () => {
|
|
1997
|
+
const couch = await db_module.get_con('xuda_servers');
|
|
1998
|
+
const res = await couch.list({ include_docs: true });
|
|
1999
|
+
return (res.rows || []).map((r) => r.doc).filter((d) => d && !String(d._id).startsWith('_design'));
|
|
2000
|
+
};
|
|
2001
|
+
|
|
2002
|
+
// Normalize one xuda_servers doc into the admin list view.
|
|
2003
|
+
const _srv_view = (doc) => {
|
|
2004
|
+
const info = doc.server_info && typeof doc.server_info === 'object' ? doc.server_info : {};
|
|
2005
|
+
return {
|
|
2006
|
+
id: doc._id,
|
|
2007
|
+
role: _srv_role(doc),
|
|
2008
|
+
deployable: _srv_is_deployable(doc),
|
|
2009
|
+
provider: doc.provider || (doc.droplet_id ? 'digitalocean' : doc.node || doc.api_url ? 'proxmox' : 'unknown'),
|
|
2010
|
+
hostname: doc.hostname || info.public_hostname || null,
|
|
2011
|
+
public_ip: doc.public || info.public_ip || null,
|
|
2012
|
+
private_ip: doc.private || info.private_ip || null, // WireGuard mesh IP (10.8.0.x)
|
|
2013
|
+
server_status: doc.server_status || info.status || 'unknown', // active | off
|
|
2014
|
+
server_is_full: !!doc.server_is_full,
|
|
2015
|
+
capacity: doc.server_capacity || null,
|
|
2016
|
+
countries: doc.countries || null,
|
|
2017
|
+
deploy: {
|
|
2018
|
+
state: _srv_deploy_state(doc),
|
|
2019
|
+
busy: _srv_busy(doc),
|
|
2020
|
+
publish_mode: doc.publish_mode || null,
|
|
2021
|
+
stat: typeof doc.stat === 'number' ? doc.stat : null,
|
|
2022
|
+
stat_ts: doc.stat_ts || null,
|
|
2023
|
+
error: doc.publish_error || null,
|
|
2024
|
+
requested_by: doc.deploy_requested_by || null,
|
|
2025
|
+
requested_ts: doc.deploy_requested_ts || null,
|
|
2026
|
+
},
|
|
2027
|
+
};
|
|
2028
|
+
};
|
|
2029
|
+
|
|
2030
|
+
// List every fleet server (regions + farm nodes) with live status + deploy
|
|
2031
|
+
// state. Read-only. Live status is maintained by controller_module.check_live_servers.
|
|
2032
|
+
export const ops_list_servers = async function (req = {}) {
|
|
2033
|
+
try {
|
|
2034
|
+
if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
|
|
2035
|
+
const docs = await _load_all_server_docs();
|
|
2036
|
+
const rank = { region: 0, farm_node: 1, farm_vm: 2, other: 3 };
|
|
2037
|
+
const servers = docs
|
|
2038
|
+
.map(_srv_view)
|
|
2039
|
+
.sort((a, b) => rank[a.role] - rank[b.role] || (a.id === 'master' ? -1 : b.id === 'master' ? 1 : String(a.id).localeCompare(String(b.id))));
|
|
2040
|
+
return {
|
|
2041
|
+
code: 1,
|
|
2042
|
+
data: {
|
|
2043
|
+
servers,
|
|
2044
|
+
deploy_targets: servers.filter((s) => s.deployable).map((s) => s.id),
|
|
2045
|
+
orchestrator: 'dev.xuda.ai',
|
|
2046
|
+
note: 'Deploy runs from the dev orchestrator; region app-servers only. Soft = zero-downtime drain + reload; hard = teardown + clean install.',
|
|
2047
|
+
},
|
|
2048
|
+
};
|
|
2049
|
+
} catch (err) { return { code: -1, data: err.message }; }
|
|
2050
|
+
};
|
|
2051
|
+
|
|
2052
|
+
// Queue a code deploy to ONE region app-server. Enqueue-only + superuser-gated:
|
|
2053
|
+
// sets stat:1 + publish_mode on the target doc; the dev orchestrator rolls it
|
|
2054
|
+
// out. mode: 'soft' (default, zero-downtime) | 'hard' (teardown + clean
|
|
2055
|
+
// install). Guards: deployable target only; not the dev orchestrator; not
|
|
2056
|
+
// already busy; confirm required (hard requires the server id typed back).
|
|
2057
|
+
// dry_run returns the plan WITHOUT writing (safe preview).
|
|
2058
|
+
export const ops_deploy_server = async function (req = {}) {
|
|
2059
|
+
try {
|
|
2060
|
+
if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
|
|
2061
|
+
const server_id = String(req.server_id || '').trim();
|
|
2062
|
+
const mode = req.mode === 'hard' ? 'hard' : 'soft';
|
|
2063
|
+
if (!server_id) return { code: -1, data: 'server_id is required' };
|
|
2064
|
+
if (server_id === 'dev') return { code: -1, data: 'the dev orchestrator deploys via rsync, not this rollout; pick a region (master/us/eu)' };
|
|
2065
|
+
|
|
2066
|
+
// FULL doc (no field projection) so it is safe to save back.
|
|
2067
|
+
const sr = await db_module.get_couch_doc('xuda_servers', server_id);
|
|
2068
|
+
if (sr.code < 0 || !sr.data) return { code: -1, data: `server "${server_id}" not found` };
|
|
2069
|
+
const doc = sr.data;
|
|
2070
|
+
if (!_srv_is_deployable(doc)) return { code: -1, data: `server "${server_id}" is not a deployable region app-server (needs docType 'regional_server' + application_server)` };
|
|
2071
|
+
|
|
2072
|
+
// Confirmation gate — proportional to blast radius.
|
|
2073
|
+
if (mode === 'hard') {
|
|
2074
|
+
if (req.confirm !== server_id) return { code: -1, data: `hard deploy tears down and reinstalls "${server_id}" (brief downtime). Re-send with confirm:"${server_id}" to proceed.` };
|
|
2075
|
+
} else {
|
|
2076
|
+
const ok = req.confirm === true || req.confirm === 'true' || req.confirm === server_id;
|
|
2077
|
+
if (!ok) return { code: -1, data: `confirm required: re-send with confirm:true to deploy "${server_id}" (soft, zero-downtime).` };
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// Already-in-flight guard.
|
|
2081
|
+
if (_srv_busy(doc)) return { code: -1, data: `a deploy is already ${_srv_deploy_state(doc)} for "${server_id}"; wait for it to finish (poll ops_server_deploy_status).` };
|
|
2082
|
+
|
|
2083
|
+
const plan = { server_id, mode, current_state: _srv_deploy_state(doc), private_ip: doc.private || null };
|
|
2084
|
+
if (req.dry_run) return { code: 1, data: { dry_run: true, would_deploy: plan } };
|
|
2085
|
+
|
|
2086
|
+
// Enqueue: the dev orchestrator's 1-min publish tick picks stat:1 up.
|
|
2087
|
+
doc.stat = 1;
|
|
2088
|
+
doc.stat_ts = Date.now();
|
|
2089
|
+
doc.publish_mode = mode;
|
|
2090
|
+
doc.deploy_requested_by = req.uid;
|
|
2091
|
+
doc.deploy_requested_ts = Date.now();
|
|
2092
|
+
delete doc.publish_error;
|
|
2093
|
+
const wr = await db_module.save_couch_doc('xuda_servers', doc);
|
|
2094
|
+
if (wr.code < 0) return { code: -1, data: wr.data };
|
|
2095
|
+
|
|
2096
|
+
_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);
|
|
2097
|
+
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}` });
|
|
2098
|
+
|
|
2099
|
+
return { code: 1, data: { enqueued: true, server_id, mode, state: 'queued', orchestrator: 'dev.xuda.ai', poll: 'ops_server_deploy_status' } };
|
|
2100
|
+
} catch (err) { return { code: -1, data: err.message }; }
|
|
2101
|
+
};
|
|
2102
|
+
|
|
2103
|
+
// Poll deploy status/progress for one server (or all region app-servers). Reads
|
|
2104
|
+
// the stat state-machine the rollout writes onto each xuda_servers doc.
|
|
2105
|
+
export const ops_server_deploy_status = async function (req = {}) {
|
|
2106
|
+
try {
|
|
2107
|
+
if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
|
|
2108
|
+
const now = Date.now();
|
|
2109
|
+
const to_status = (doc) => ({
|
|
2110
|
+
id: doc._id,
|
|
2111
|
+
state: _srv_deploy_state(doc),
|
|
2112
|
+
busy: _srv_busy(doc),
|
|
2113
|
+
publish_mode: doc.publish_mode || null,
|
|
2114
|
+
stat: typeof doc.stat === 'number' ? doc.stat : null,
|
|
2115
|
+
stat_ts: doc.stat_ts || null,
|
|
2116
|
+
elapsed_ms: doc.stat_ts && _srv_busy(doc) ? now - doc.stat_ts : null,
|
|
2117
|
+
error: doc.publish_error || null,
|
|
2118
|
+
requested_by: doc.deploy_requested_by || null,
|
|
2119
|
+
requested_ts: doc.deploy_requested_ts || null,
|
|
2120
|
+
});
|
|
2121
|
+
|
|
2122
|
+
if (req.server_id) {
|
|
2123
|
+
const sr = await db_module.get_couch_doc('xuda_servers', String(req.server_id).trim());
|
|
2124
|
+
if (sr.code < 0 || !sr.data) return { code: -1, data: `server "${req.server_id}" not found` };
|
|
2125
|
+
return { code: 1, data: to_status(sr.data) };
|
|
2126
|
+
}
|
|
2127
|
+
const docs = await _load_all_server_docs();
|
|
2128
|
+
const rows = docs.filter(_srv_is_deployable).map(to_status);
|
|
2129
|
+
return { code: 1, data: { servers: rows, any_busy: rows.some((r) => r.busy) } };
|
|
2130
|
+
} catch (err) { return { code: -1, data: err.message }; }
|
|
2131
|
+
};
|
|
2132
|
+
|
|
1960
2133
|
const TIPS_FILE_PATH = path.join(process.env.XUDA_HOME, 'dist', 'tips', 'tips.json');
|
|
1961
2134
|
let _did_you_know_tips_cache = null;
|
|
1962
2135
|
let _did_you_know_tips_cache_mtime = 0;
|
|
@@ -2492,8 +2665,40 @@ export const add_app_log_util = async function (body, status, service, source, r
|
|
|
2492
2665
|
}
|
|
2493
2666
|
};
|
|
2494
2667
|
|
|
2668
|
+
// Append one entry to the SSH access log (xuda_ssh_keys, docType ssh_key_event).
|
|
2669
|
+
// stat:3 is stamped so the existing idx_ssh_keys_uid [docType, uid, stat] index
|
|
2670
|
+
// serves the log reads. deploy_module carries the same writer on purpose: cpi
|
|
2671
|
+
// modules do not import each other.
|
|
2672
|
+
const _ssh_key_event = async function (uid, action, extra = {}) {
|
|
2673
|
+
try {
|
|
2674
|
+
if (!uid) return;
|
|
2675
|
+
await db_module.save_couch_doc('xuda_ssh_keys', {
|
|
2676
|
+
_id: await _common.xuda_get_uuid('ssh_key_event'),
|
|
2677
|
+
docType: 'ssh_key_event',
|
|
2678
|
+
uid, action, stat: 3, at: Date.now(), ...extra,
|
|
2679
|
+
});
|
|
2680
|
+
} catch (e) {}
|
|
2681
|
+
};
|
|
2682
|
+
|
|
2683
|
+
// Pull a key off every server it is installed on (assigned_apps is the reverse
|
|
2684
|
+
// index deploy_module maintains at assign time). Best-effort per app: one
|
|
2685
|
+
// unreachable server must not leave the key half-revoked on the others.
|
|
2686
|
+
const _ssh_key_revoke_everywhere = async function (doc) {
|
|
2687
|
+
const apps = Array.isArray(doc.assigned_apps) ? doc.assigned_apps : [];
|
|
2688
|
+
const failed = [];
|
|
2689
|
+
for (const app_id of apps) {
|
|
2690
|
+
try {
|
|
2691
|
+
const r = await deploy_ms.revoke_ssh_keys({ app_id, ssh_keys: [doc._id] });
|
|
2692
|
+
if (!(r && r.code > 0)) failed.push(app_id);
|
|
2693
|
+
} catch (e) {
|
|
2694
|
+
failed.push(app_id);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
return { apps, failed };
|
|
2698
|
+
};
|
|
2699
|
+
|
|
2495
2700
|
export const save_ssh_key = async function (req) {
|
|
2496
|
-
const { uid, _id, ssh_key_name, ssh_key_content } = req;
|
|
2701
|
+
const { uid, _id, ssh_key_name, ssh_key_content, expires_at, enabled } = req;
|
|
2497
2702
|
|
|
2498
2703
|
function isValidSSHPublicKey(sshPublicKey) {
|
|
2499
2704
|
// Regular expression to match SSH public keys
|
|
@@ -2506,6 +2711,16 @@ export const save_ssh_key = async function (req) {
|
|
|
2506
2711
|
return { code: -1, data: 'invalid ssh key' };
|
|
2507
2712
|
}
|
|
2508
2713
|
|
|
2714
|
+
// Optional expiry (epoch ms). null/0 clears it; a date in the past is a
|
|
2715
|
+
// mistake the UI should hear about, not silently store.
|
|
2716
|
+
let _exp = null;
|
|
2717
|
+
if (expires_at !== undefined && expires_at !== null && expires_at !== 0 && expires_at !== '') {
|
|
2718
|
+
_exp = Number(expires_at);
|
|
2719
|
+
if (!Number.isFinite(_exp) || _exp <= Date.now()) {
|
|
2720
|
+
return { code: -1, data: 'expires_at must be a future date (epoch milliseconds)' };
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2509
2724
|
var doc = {
|
|
2510
2725
|
uid,
|
|
2511
2726
|
docType: 'ssh_key',
|
|
@@ -2514,36 +2729,92 @@ export const save_ssh_key = async function (req) {
|
|
|
2514
2729
|
stat: 3,
|
|
2515
2730
|
};
|
|
2516
2731
|
|
|
2732
|
+
var is_new = true;
|
|
2517
2733
|
if (_id) {
|
|
2518
2734
|
const ret = await db_module.get_couch_doc('xuda_ssh_keys', _id);
|
|
2519
2735
|
if (ret.code < 0) {
|
|
2520
2736
|
return ret;
|
|
2521
2737
|
}
|
|
2738
|
+
if (ret.data.uid !== uid) {
|
|
2739
|
+
return { code: -403, data: 'not your key' };
|
|
2740
|
+
}
|
|
2522
2741
|
doc = ret.data;
|
|
2523
2742
|
doc.date_updated_ts = Date.now();
|
|
2743
|
+
is_new = false;
|
|
2524
2744
|
} else {
|
|
2525
2745
|
doc._id = await _common.xuda_get_uuid('ssh_key');
|
|
2526
2746
|
}
|
|
2527
2747
|
|
|
2528
2748
|
doc.ssh_key_name = ssh_key_name;
|
|
2529
2749
|
doc.ssh_key_content = ssh_key_content;
|
|
2750
|
+
if (expires_at !== undefined) doc.expires_at = _exp;
|
|
2751
|
+
if (enabled !== undefined) doc.enabled = enabled !== false;
|
|
2752
|
+
if (doc.enabled === undefined) doc.enabled = true;
|
|
2530
2753
|
|
|
2531
2754
|
const save_ret = await db_module.save_couch_doc('xuda_ssh_keys', doc);
|
|
2755
|
+
if (save_ret.code > -1) {
|
|
2756
|
+
await _ssh_key_event(uid, is_new ? 'create' : 'update', { ssh_key_id: doc._id, key_name: ssh_key_name, expires_at: doc.expires_at || null });
|
|
2757
|
+
}
|
|
2532
2758
|
|
|
2533
2759
|
return save_ret;
|
|
2534
2760
|
};
|
|
2761
|
+
|
|
2762
|
+
// On/off switch for one key. Switching OFF also pulls the key from every server
|
|
2763
|
+
// it is installed on, so "off" means the developer is locked out now, not on the
|
|
2764
|
+
// next reassign. Switching ON does not auto-reinstall: the user re-assigns
|
|
2765
|
+
// per server (deliberate: re-granting access is an explicit act).
|
|
2766
|
+
export const set_ssh_key_enabled = async function (req) {
|
|
2767
|
+
const { uid, ssh_key_id, enabled } = req;
|
|
2768
|
+
const ret = await db_module.get_couch_doc('xuda_ssh_keys', ssh_key_id);
|
|
2769
|
+
if (ret.code < 0) return ret;
|
|
2770
|
+
const doc = ret.data;
|
|
2771
|
+
if (doc.uid !== uid) return { code: -403, data: 'not your key' };
|
|
2772
|
+
|
|
2773
|
+
const on = enabled !== false;
|
|
2774
|
+
let revoked = null;
|
|
2775
|
+
if (!on) {
|
|
2776
|
+
revoked = await _ssh_key_revoke_everywhere(doc);
|
|
2777
|
+
if (revoked.failed.length) {
|
|
2778
|
+
return { code: -1, data: `could not remove the key from ${revoked.failed.length} server(s) (${revoked.failed.join(', ')}). The key stays ON so the state on your servers is never ambiguous. Try again.` };
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
// revoke_ssh_keys rewrote the key doc (assigned_apps); refetch before saving.
|
|
2782
|
+
const fresh = await db_module.get_couch_doc('xuda_ssh_keys', ssh_key_id);
|
|
2783
|
+
const fdoc = fresh.code > -1 ? fresh.data : doc;
|
|
2784
|
+
fdoc.enabled = on;
|
|
2785
|
+
fdoc.date_updated_ts = Date.now();
|
|
2786
|
+
const save_ret = await db_module.save_couch_doc('xuda_ssh_keys', fdoc);
|
|
2787
|
+
if (save_ret.code > -1) {
|
|
2788
|
+
await _ssh_key_event(uid, on ? 'enable' : 'disable', { ssh_key_id, key_name: fdoc.ssh_key_name, ...(revoked ? { removed_from: revoked.apps } : {}) });
|
|
2789
|
+
}
|
|
2790
|
+
return save_ret.code > -1 ? { code: 1, data: { enabled: on, removed_from: revoked ? revoked.apps : [] } } : save_ret;
|
|
2791
|
+
};
|
|
2792
|
+
|
|
2535
2793
|
export const delete_ssh_key = async function (req) {
|
|
2536
|
-
const { ssh_key_id } = req;
|
|
2794
|
+
const { uid, ssh_key_id } = req;
|
|
2537
2795
|
|
|
2538
2796
|
const ret = await db_module.get_couch_doc('xuda_ssh_keys', ssh_key_id);
|
|
2539
2797
|
if (ret.code < 0) {
|
|
2540
2798
|
return ret;
|
|
2541
2799
|
}
|
|
2542
2800
|
var doc = ret.data;
|
|
2801
|
+
if (doc.uid !== uid) {
|
|
2802
|
+
return { code: -403, data: 'not your key' };
|
|
2803
|
+
}
|
|
2804
|
+
// A deleted key must stop opening doors: pull it off every server first.
|
|
2805
|
+
const revoked = await _ssh_key_revoke_everywhere(doc);
|
|
2806
|
+
if (revoked.failed.length) {
|
|
2807
|
+
return { code: -1, data: `could not remove the key from ${revoked.failed.length} server(s) (${revoked.failed.join(', ')}). The key was NOT deleted. Try again.` };
|
|
2808
|
+
}
|
|
2809
|
+
const fresh = await db_module.get_couch_doc('xuda_ssh_keys', ssh_key_id);
|
|
2810
|
+
doc = fresh.code > -1 ? fresh.data : doc;
|
|
2543
2811
|
doc.date_updated_ts = Date.now();
|
|
2544
2812
|
doc.stat = 4;
|
|
2545
2813
|
|
|
2546
2814
|
const save_ret = await db_module.save_couch_doc('xuda_ssh_keys', doc);
|
|
2815
|
+
if (save_ret.code > -1) {
|
|
2816
|
+
await _ssh_key_event(uid, 'delete', { ssh_key_id, key_name: doc.ssh_key_name, ...(revoked.apps.length ? { removed_from: revoked.apps } : {}) });
|
|
2817
|
+
}
|
|
2547
2818
|
|
|
2548
2819
|
return save_ret;
|
|
2549
2820
|
};
|
|
@@ -2563,6 +2834,22 @@ export const get_ssh_keys = async function (req) {
|
|
|
2563
2834
|
return ret;
|
|
2564
2835
|
};
|
|
2565
2836
|
|
|
2837
|
+
// The account's SSH access log: every grant, revoke, on/off flip, expiry and
|
|
2838
|
+
// delete, newest first. Selector rides idx_ssh_keys_uid ([docType, uid, stat]).
|
|
2839
|
+
export const get_ssh_key_events = async function (req) {
|
|
2840
|
+
const { uid, ssh_key_id, app_id, limit } = req;
|
|
2841
|
+
const opt = {
|
|
2842
|
+
selector: { docType: 'ssh_key_event', uid, stat: { $gte: 0 } },
|
|
2843
|
+
limit: 2000,
|
|
2844
|
+
};
|
|
2845
|
+
if (ssh_key_id) opt.selector.ssh_key_id = ssh_key_id;
|
|
2846
|
+
if (app_id) opt.selector.app_id = app_id;
|
|
2847
|
+
const ret = await db_module.find_couch_query('xuda_ssh_keys', opt);
|
|
2848
|
+
if (ret.code < 0) return ret;
|
|
2849
|
+
const rows = (ret.docs || ret.data?.docs || []).sort((a, b) => (b.at || 0) - (a.at || 0)).slice(0, Math.min(Number(limit) || 200, 1000));
|
|
2850
|
+
return { code: 1, data: rows };
|
|
2851
|
+
};
|
|
2852
|
+
|
|
2566
2853
|
export const search_users = async function (req) {
|
|
2567
2854
|
const { _id, search, limit, skip, bookmark, uid } = req;
|
|
2568
2855
|
|
|
@@ -5363,6 +5650,8 @@ export const update_account_profile = async function (req, job_id, headers) {
|
|
|
5363
5650
|
'active_ai_model',
|
|
5364
5651
|
'ai_models',
|
|
5365
5652
|
'active_agents',
|
|
5653
|
+
// Email Template tab: { style: 'plain' | 'card' | 'accent' | 'minimal' }
|
|
5654
|
+
'email_template',
|
|
5366
5655
|
];
|
|
5367
5656
|
try {
|
|
5368
5657
|
let { data: account_profile_doc } = await db_module.get_app_couch_doc(app_id, _id);
|
|
@@ -5746,10 +6035,21 @@ export const record_ai_usage = async function (uid, input_tokens, output_tokens,
|
|
|
5746
6035
|
try {
|
|
5747
6036
|
if (!account_profile_info) throw new Error('missing account_profile_info');
|
|
5748
6037
|
|
|
5749
|
-
|
|
6038
|
+
// `model` arrives as the stable ai_models code (the key profiles/agents store,
|
|
6039
|
+
// e.g. 'gpt-5.4'). Price by that code, but store the real OpenAI model name in
|
|
6040
|
+
// the usage record (e.g. 'gpt-5.6-terra') so usage/billing reflects the actual
|
|
6041
|
+
// model used. Also match by .model so a caller that already passed a real id
|
|
6042
|
+
// still prices correctly.
|
|
6043
|
+
const models = _conf?.ai_models || {};
|
|
6044
|
+
// Canonicalize a legacy key (still stored on old profiles/agents, e.g. 'gpt-5.4')
|
|
6045
|
+
// to its current catalog code before pricing.
|
|
6046
|
+
const code = _conf?.ai_model_aliases?.[model] || model;
|
|
6047
|
+
const entry = models[code] || Object.values(models).find((e) => e && e.model === model) || null;
|
|
6048
|
+
const cost = entry || {
|
|
5750
6049
|
input: 1.0,
|
|
5751
6050
|
output: 1.0,
|
|
5752
6051
|
};
|
|
6052
|
+
const model_name = entry?.model || model;
|
|
5753
6053
|
|
|
5754
6054
|
const t = Date.now();
|
|
5755
6055
|
let usage_doc = {
|
|
@@ -5763,7 +6063,7 @@ export const record_ai_usage = async function (uid, input_tokens, output_tokens,
|
|
|
5763
6063
|
output_tokens,
|
|
5764
6064
|
stat: 3,
|
|
5765
6065
|
metadata,
|
|
5766
|
-
model,
|
|
6066
|
+
model: model_name,
|
|
5767
6067
|
cost,
|
|
5768
6068
|
account_profile_info,
|
|
5769
6069
|
tools,
|
|
@@ -6055,6 +6355,33 @@ const _nl_gif_url = (g) => (g ? `https://xuda.ai/dist/images/email/${g}.gif` : '
|
|
|
6055
6355
|
const _nl_esc = (v) =>
|
|
6056
6356
|
String(v == null ? '' : v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
6057
6357
|
|
|
6358
|
+
// Self-healing storage: xuda_newsletter lives on master ONLY and replicates to
|
|
6359
|
+
// master2 ONLY (never regions — the fleet control-DB hub must not pick it up).
|
|
6360
|
+
// Creates the DB if missing (fresh master build / DR restore / master2 promote)
|
|
6361
|
+
// and upserts the continuous master->master2 replicator doc, modeled on the
|
|
6362
|
+
// fw_repl_* docs. Idempotent; called from newsletter_generate which is already
|
|
6363
|
+
// gated to master + dev, so this can never materialize the DB on a region.
|
|
6364
|
+
const _nl_ensure_storage = async () => {
|
|
6365
|
+
const cc = _conf.couchdb_control_database?.[process.env.XUDA_HOSTNAME] || _conf.couchdb_control_database;
|
|
6366
|
+
if (!cc?.db_server_location) return;
|
|
6367
|
+
await db_module.create_couch_database(NEWSLETTER_DB, `${cc.db_server_http}://${cc.db_server_username}:${cc.db_server_password}@${cc.db_server_location}`);
|
|
6368
|
+
if (process.env.XUDA_HOSTNAME !== 'master.xuda.ai') return;
|
|
6369
|
+
const m2 = _conf.couchdb_control_database?.['master2.xuda.ai'];
|
|
6370
|
+
if (!m2?.db_server_location) return;
|
|
6371
|
+
const rep_id = 'nl_repl_master_to_master2';
|
|
6372
|
+
const existing = await db_module.get_couch_doc('_replicator', rep_id);
|
|
6373
|
+
if (existing.code > 0 && existing.data) return;
|
|
6374
|
+
const b64 = (u, p) => 'Basic ' + Buffer.from(`${u}:${p}`).toString('base64');
|
|
6375
|
+
const save = await db_module.save_couch_doc('_replicator', {
|
|
6376
|
+
_id: rep_id,
|
|
6377
|
+
source: { url: `${cc.db_server_http}://${cc.db_server_location}/${NEWSLETTER_DB}`, headers: { Authorization: b64(cc.db_server_username, cc.db_server_password) } },
|
|
6378
|
+
target: { url: `${m2.db_server_http}://${m2.db_server_location}/${NEWSLETTER_DB}`, headers: { Authorization: b64(m2.db_server_username || cc.db_server_username, m2.db_server_password || cc.db_server_password) } },
|
|
6379
|
+
continuous: true,
|
|
6380
|
+
create_target: true,
|
|
6381
|
+
});
|
|
6382
|
+
if (save.code < 0) console.error('[newsletter] replicator ensure failed:', save.data);
|
|
6383
|
+
};
|
|
6384
|
+
|
|
6058
6385
|
// Next issue number = max(existing) + 1. Read-only projection (never PUT these).
|
|
6059
6386
|
const _nl_next_number = async () => {
|
|
6060
6387
|
try {
|
|
@@ -6412,6 +6739,10 @@ const _newsletter_send_sample_internal = async (issue, to_email, theme) => {
|
|
|
6412
6739
|
export const newsletter_generate = async function (req = {}) {
|
|
6413
6740
|
try {
|
|
6414
6741
|
if (!req.system && !_ops_is_super(req)) return { code: -403, data: 'superuser only' };
|
|
6742
|
+
// Master-only (dev exempt for testing): the issue DB must never be created
|
|
6743
|
+
// on a regional couch, and the weekly coupon/sample flow belongs to master.
|
|
6744
|
+
if (!_conf.is_debug && process.env.XUDA_HOSTNAME !== 'master.xuda.ai') return { code: -1, data: 'newsletter_generate runs on master only' };
|
|
6745
|
+
await _nl_ensure_storage();
|
|
6415
6746
|
|
|
6416
6747
|
// Two DISTINCT tips: one for "Did you know?", one for "Tip of the week".
|
|
6417
6748
|
const _two_tips = async () => {
|
|
@@ -6569,6 +6900,9 @@ const _newsletter_blast = async (issue, by_uid) => {
|
|
|
6569
6900
|
export const newsletter_publish = async function (req = {}) {
|
|
6570
6901
|
try {
|
|
6571
6902
|
if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
|
|
6903
|
+
// Same master-only gate as generate: the blast must run where xuda_newsletter
|
|
6904
|
+
// and the authoritative xuda_accounts live (dev exempt for testing).
|
|
6905
|
+
if (!_conf.is_debug && process.env.XUDA_HOSTNAME !== 'master.xuda.ai') return { code: -1, data: 'newsletter_publish runs on master only' };
|
|
6572
6906
|
const id = req.id || `nl_issue_${req.issue_number}`;
|
|
6573
6907
|
const gret = await db_module.get_couch_doc(NEWSLETTER_DB, id);
|
|
6574
6908
|
if (gret.code < 0 || !gret.data) return { code: -1, data: 'issue not found' };
|
package/index_ms.mjs
CHANGED
|
@@ -189,6 +189,18 @@ export const ops_send_notification = async function (...args) {
|
|
|
189
189
|
return await broker.send_to_queue("ops_send_notification", ...args);
|
|
190
190
|
};
|
|
191
191
|
|
|
192
|
+
export const ops_list_servers = async function (...args) {
|
|
193
|
+
return await broker.send_to_queue("ops_list_servers", ...args);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const ops_deploy_server = async function (...args) {
|
|
197
|
+
return await broker.send_to_queue("ops_deploy_server", ...args);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const ops_server_deploy_status = async function (...args) {
|
|
201
|
+
return await broker.send_to_queue("ops_server_deploy_status", ...args);
|
|
202
|
+
};
|
|
203
|
+
|
|
192
204
|
export const did_you_know_tips = async function (...args) {
|
|
193
205
|
return await broker.send_to_queue("did_you_know_tips", ...args);
|
|
194
206
|
};
|
|
@@ -237,6 +249,10 @@ export const save_ssh_key = async function (...args) {
|
|
|
237
249
|
return await broker.send_to_queue("save_ssh_key", ...args);
|
|
238
250
|
};
|
|
239
251
|
|
|
252
|
+
export const set_ssh_key_enabled = async function (...args) {
|
|
253
|
+
return await broker.send_to_queue("set_ssh_key_enabled", ...args);
|
|
254
|
+
};
|
|
255
|
+
|
|
240
256
|
export const delete_ssh_key = async function (...args) {
|
|
241
257
|
return await broker.send_to_queue("delete_ssh_key", ...args);
|
|
242
258
|
};
|
|
@@ -245,6 +261,10 @@ export const get_ssh_keys = async function (...args) {
|
|
|
245
261
|
return await broker.send_to_queue("get_ssh_keys", ...args);
|
|
246
262
|
};
|
|
247
263
|
|
|
264
|
+
export const get_ssh_key_events = async function (...args) {
|
|
265
|
+
return await broker.send_to_queue("get_ssh_key_events", ...args);
|
|
266
|
+
};
|
|
267
|
+
|
|
248
268
|
export const search_users = async function (...args) {
|
|
249
269
|
return await broker.send_to_queue("search_users", ...args);
|
|
250
270
|
};
|
package/index_msa.mjs
CHANGED
|
@@ -189,6 +189,18 @@ export const ops_send_notification = function (...args) {
|
|
|
189
189
|
broker.send_to_queue_async("ops_send_notification", ...args);
|
|
190
190
|
};
|
|
191
191
|
|
|
192
|
+
export const ops_list_servers = function (...args) {
|
|
193
|
+
broker.send_to_queue_async("ops_list_servers", ...args);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const ops_deploy_server = function (...args) {
|
|
197
|
+
broker.send_to_queue_async("ops_deploy_server", ...args);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const ops_server_deploy_status = function (...args) {
|
|
201
|
+
broker.send_to_queue_async("ops_server_deploy_status", ...args);
|
|
202
|
+
};
|
|
203
|
+
|
|
192
204
|
export const did_you_know_tips = function (...args) {
|
|
193
205
|
broker.send_to_queue_async("did_you_know_tips", ...args);
|
|
194
206
|
};
|
|
@@ -237,6 +249,10 @@ export const save_ssh_key = function (...args) {
|
|
|
237
249
|
broker.send_to_queue_async("save_ssh_key", ...args);
|
|
238
250
|
};
|
|
239
251
|
|
|
252
|
+
export const set_ssh_key_enabled = function (...args) {
|
|
253
|
+
broker.send_to_queue_async("set_ssh_key_enabled", ...args);
|
|
254
|
+
};
|
|
255
|
+
|
|
240
256
|
export const delete_ssh_key = function (...args) {
|
|
241
257
|
broker.send_to_queue_async("delete_ssh_key", ...args);
|
|
242
258
|
};
|
|
@@ -245,6 +261,10 @@ export const get_ssh_keys = function (...args) {
|
|
|
245
261
|
broker.send_to_queue_async("get_ssh_keys", ...args);
|
|
246
262
|
};
|
|
247
263
|
|
|
264
|
+
export const get_ssh_key_events = function (...args) {
|
|
265
|
+
broker.send_to_queue_async("get_ssh_key_events", ...args);
|
|
266
|
+
};
|
|
267
|
+
|
|
248
268
|
export const search_users = function (...args) {
|
|
249
269
|
broker.send_to_queue_async("search_users", ...args);
|
|
250
270
|
};
|