@xuda.io/account_module 1.2.2286 → 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 +167 -4
- package/index_ms.mjs +8 -0
- package/index_msa.mjs +8 -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
|
|
|
@@ -2663,8 +2665,40 @@ export const add_app_log_util = async function (body, status, service, source, r
|
|
|
2663
2665
|
}
|
|
2664
2666
|
};
|
|
2665
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
|
+
|
|
2666
2700
|
export const save_ssh_key = async function (req) {
|
|
2667
|
-
const { uid, _id, ssh_key_name, ssh_key_content } = req;
|
|
2701
|
+
const { uid, _id, ssh_key_name, ssh_key_content, expires_at, enabled } = req;
|
|
2668
2702
|
|
|
2669
2703
|
function isValidSSHPublicKey(sshPublicKey) {
|
|
2670
2704
|
// Regular expression to match SSH public keys
|
|
@@ -2677,6 +2711,16 @@ export const save_ssh_key = async function (req) {
|
|
|
2677
2711
|
return { code: -1, data: 'invalid ssh key' };
|
|
2678
2712
|
}
|
|
2679
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
|
+
|
|
2680
2724
|
var doc = {
|
|
2681
2725
|
uid,
|
|
2682
2726
|
docType: 'ssh_key',
|
|
@@ -2685,36 +2729,92 @@ export const save_ssh_key = async function (req) {
|
|
|
2685
2729
|
stat: 3,
|
|
2686
2730
|
};
|
|
2687
2731
|
|
|
2732
|
+
var is_new = true;
|
|
2688
2733
|
if (_id) {
|
|
2689
2734
|
const ret = await db_module.get_couch_doc('xuda_ssh_keys', _id);
|
|
2690
2735
|
if (ret.code < 0) {
|
|
2691
2736
|
return ret;
|
|
2692
2737
|
}
|
|
2738
|
+
if (ret.data.uid !== uid) {
|
|
2739
|
+
return { code: -403, data: 'not your key' };
|
|
2740
|
+
}
|
|
2693
2741
|
doc = ret.data;
|
|
2694
2742
|
doc.date_updated_ts = Date.now();
|
|
2743
|
+
is_new = false;
|
|
2695
2744
|
} else {
|
|
2696
2745
|
doc._id = await _common.xuda_get_uuid('ssh_key');
|
|
2697
2746
|
}
|
|
2698
2747
|
|
|
2699
2748
|
doc.ssh_key_name = ssh_key_name;
|
|
2700
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;
|
|
2701
2753
|
|
|
2702
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
|
+
}
|
|
2703
2758
|
|
|
2704
2759
|
return save_ret;
|
|
2705
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
|
+
|
|
2706
2793
|
export const delete_ssh_key = async function (req) {
|
|
2707
|
-
const { ssh_key_id } = req;
|
|
2794
|
+
const { uid, ssh_key_id } = req;
|
|
2708
2795
|
|
|
2709
2796
|
const ret = await db_module.get_couch_doc('xuda_ssh_keys', ssh_key_id);
|
|
2710
2797
|
if (ret.code < 0) {
|
|
2711
2798
|
return ret;
|
|
2712
2799
|
}
|
|
2713
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;
|
|
2714
2811
|
doc.date_updated_ts = Date.now();
|
|
2715
2812
|
doc.stat = 4;
|
|
2716
2813
|
|
|
2717
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
|
+
}
|
|
2718
2818
|
|
|
2719
2819
|
return save_ret;
|
|
2720
2820
|
};
|
|
@@ -2734,6 +2834,22 @@ export const get_ssh_keys = async function (req) {
|
|
|
2734
2834
|
return ret;
|
|
2735
2835
|
};
|
|
2736
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
|
+
|
|
2737
2853
|
export const search_users = async function (req) {
|
|
2738
2854
|
const { _id, search, limit, skip, bookmark, uid } = req;
|
|
2739
2855
|
|
|
@@ -5534,6 +5650,8 @@ export const update_account_profile = async function (req, job_id, headers) {
|
|
|
5534
5650
|
'active_ai_model',
|
|
5535
5651
|
'ai_models',
|
|
5536
5652
|
'active_agents',
|
|
5653
|
+
// Email Template tab: { style: 'plain' | 'card' | 'accent' | 'minimal' }
|
|
5654
|
+
'email_template',
|
|
5537
5655
|
];
|
|
5538
5656
|
try {
|
|
5539
5657
|
let { data: account_profile_doc } = await db_module.get_app_couch_doc(app_id, _id);
|
|
@@ -5917,10 +6035,21 @@ export const record_ai_usage = async function (uid, input_tokens, output_tokens,
|
|
|
5917
6035
|
try {
|
|
5918
6036
|
if (!account_profile_info) throw new Error('missing account_profile_info');
|
|
5919
6037
|
|
|
5920
|
-
|
|
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 || {
|
|
5921
6049
|
input: 1.0,
|
|
5922
6050
|
output: 1.0,
|
|
5923
6051
|
};
|
|
6052
|
+
const model_name = entry?.model || model;
|
|
5924
6053
|
|
|
5925
6054
|
const t = Date.now();
|
|
5926
6055
|
let usage_doc = {
|
|
@@ -5934,7 +6063,7 @@ export const record_ai_usage = async function (uid, input_tokens, output_tokens,
|
|
|
5934
6063
|
output_tokens,
|
|
5935
6064
|
stat: 3,
|
|
5936
6065
|
metadata,
|
|
5937
|
-
model,
|
|
6066
|
+
model: model_name,
|
|
5938
6067
|
cost,
|
|
5939
6068
|
account_profile_info,
|
|
5940
6069
|
tools,
|
|
@@ -6226,6 +6355,33 @@ const _nl_gif_url = (g) => (g ? `https://xuda.ai/dist/images/email/${g}.gif` : '
|
|
|
6226
6355
|
const _nl_esc = (v) =>
|
|
6227
6356
|
String(v == null ? '' : v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
6228
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
|
+
|
|
6229
6385
|
// Next issue number = max(existing) + 1. Read-only projection (never PUT these).
|
|
6230
6386
|
const _nl_next_number = async () => {
|
|
6231
6387
|
try {
|
|
@@ -6583,6 +6739,10 @@ const _newsletter_send_sample_internal = async (issue, to_email, theme) => {
|
|
|
6583
6739
|
export const newsletter_generate = async function (req = {}) {
|
|
6584
6740
|
try {
|
|
6585
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();
|
|
6586
6746
|
|
|
6587
6747
|
// Two DISTINCT tips: one for "Did you know?", one for "Tip of the week".
|
|
6588
6748
|
const _two_tips = async () => {
|
|
@@ -6740,6 +6900,9 @@ const _newsletter_blast = async (issue, by_uid) => {
|
|
|
6740
6900
|
export const newsletter_publish = async function (req = {}) {
|
|
6741
6901
|
try {
|
|
6742
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' };
|
|
6743
6906
|
const id = req.id || `nl_issue_${req.issue_number}`;
|
|
6744
6907
|
const gret = await db_module.get_couch_doc(NEWSLETTER_DB, id);
|
|
6745
6908
|
if (gret.code < 0 || !gret.data) return { code: -1, data: 'issue not found' };
|
package/index_ms.mjs
CHANGED
|
@@ -249,6 +249,10 @@ export const save_ssh_key = async function (...args) {
|
|
|
249
249
|
return await broker.send_to_queue("save_ssh_key", ...args);
|
|
250
250
|
};
|
|
251
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
|
+
|
|
252
256
|
export const delete_ssh_key = async function (...args) {
|
|
253
257
|
return await broker.send_to_queue("delete_ssh_key", ...args);
|
|
254
258
|
};
|
|
@@ -257,6 +261,10 @@ export const get_ssh_keys = async function (...args) {
|
|
|
257
261
|
return await broker.send_to_queue("get_ssh_keys", ...args);
|
|
258
262
|
};
|
|
259
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
|
+
|
|
260
268
|
export const search_users = async function (...args) {
|
|
261
269
|
return await broker.send_to_queue("search_users", ...args);
|
|
262
270
|
};
|
package/index_msa.mjs
CHANGED
|
@@ -249,6 +249,10 @@ export const save_ssh_key = function (...args) {
|
|
|
249
249
|
broker.send_to_queue_async("save_ssh_key", ...args);
|
|
250
250
|
};
|
|
251
251
|
|
|
252
|
+
export const set_ssh_key_enabled = function (...args) {
|
|
253
|
+
broker.send_to_queue_async("set_ssh_key_enabled", ...args);
|
|
254
|
+
};
|
|
255
|
+
|
|
252
256
|
export const delete_ssh_key = function (...args) {
|
|
253
257
|
broker.send_to_queue_async("delete_ssh_key", ...args);
|
|
254
258
|
};
|
|
@@ -257,6 +261,10 @@ export const get_ssh_keys = function (...args) {
|
|
|
257
261
|
broker.send_to_queue_async("get_ssh_keys", ...args);
|
|
258
262
|
};
|
|
259
263
|
|
|
264
|
+
export const get_ssh_key_events = function (...args) {
|
|
265
|
+
broker.send_to_queue_async("get_ssh_key_events", ...args);
|
|
266
|
+
};
|
|
267
|
+
|
|
260
268
|
export const search_users = function (...args) {
|
|
261
269
|
broker.send_to_queue_async("search_users", ...args);
|
|
262
270
|
};
|