@xuda.io/ai_module 1.1.5647 → 1.1.5648
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 +61 -916
- package/index_ms.mjs +4 -68
- package/index_msa.mjs +4 -68
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -354,6 +354,9 @@ const email_ms = await import(`${module_path}/email_module/index_ms.mjs`);
|
|
|
354
354
|
const voice_ms = await import(`${module_path}/voice_module/index_ms.mjs`);
|
|
355
355
|
const api_ms = await import(`${module_path}/api_module/index_ms.mjs`);
|
|
356
356
|
const stripe_ms = await import(`${module_path}/stripe_module/index_ms.mjs`);
|
|
357
|
+
// The "I'm not a robot" engine moved out to its own module; the embedded
|
|
358
|
+
// contact form is the only thing left here that asks it anything.
|
|
359
|
+
const bot_ms = await import(`${module_path}/bot_protection_module/index_ms.mjs`);
|
|
357
360
|
|
|
358
361
|
const ws_dashboard_msa = await import(`${module_path}/ws_dashboard_module/index_msa.mjs`);
|
|
359
362
|
const account_msa = await import(`${module_path}/account_module/index_msa.mjs`);
|
|
@@ -4611,6 +4614,32 @@ export const submit_chat_gpt_prompt = async function (req) {
|
|
|
4611
4614
|
}
|
|
4612
4615
|
};
|
|
4613
4616
|
|
|
4617
|
+
// Structured output for OTHER modules, over the broker. submit_chat_gpt_prompt
|
|
4618
|
+
// takes a zod schema in response_format, and a zod object cannot survive the
|
|
4619
|
+
// JSON hop between microservices, so a cross-module caller passes a plain field
|
|
4620
|
+
// spec instead and the schema is built here:
|
|
4621
|
+
// { score: 'number', verdict: ['human','bot'], reason: 'string' }
|
|
4622
|
+
// -> z.object({ score: z.number(), verdict: z.enum([...]), reason: z.string() })
|
|
4623
|
+
// Returns the PARSED object as data (code 5), not the raw JSON string.
|
|
4624
|
+
export const submit_structured_prompt = async function (req) {
|
|
4625
|
+
try {
|
|
4626
|
+
const { schema, ...rest } = req || {};
|
|
4627
|
+
if (!schema || typeof schema !== 'object') return { code: -5, data: 'schema required' };
|
|
4628
|
+
const shape = {};
|
|
4629
|
+
for (const [key, spec] of Object.entries(schema)) {
|
|
4630
|
+
if (Array.isArray(spec)) shape[key] = z.enum(spec);
|
|
4631
|
+
else if (spec === 'number') shape[key] = z.number();
|
|
4632
|
+
else if (spec === 'boolean') shape[key] = z.boolean();
|
|
4633
|
+
else shape[key] = z.string();
|
|
4634
|
+
}
|
|
4635
|
+
const ret = await submit_chat_gpt_prompt({ ...rest, response_format: z.object(shape) });
|
|
4636
|
+
if (ret.code !== 5 || !ret.data) return ret;
|
|
4637
|
+
return { code: 5, data: JSON.parse(ret.data) };
|
|
4638
|
+
} catch (err) {
|
|
4639
|
+
return { code: -5, data: err.message || String(err) };
|
|
4640
|
+
}
|
|
4641
|
+
};
|
|
4642
|
+
|
|
4614
4643
|
// external_app: turn a scan payload (from the embed engine's window.__xuda_embed
|
|
4615
4644
|
// .scan()) into a short list of concrete, helpful suggestions the user can act
|
|
4616
4645
|
// on from Xuda — the semantic layer on top of the engine's mechanical scan.
|
|
@@ -5846,6 +5875,10 @@ const chat_email = async function (req, job_id, headers) {
|
|
|
5846
5875
|
try {
|
|
5847
5876
|
const profile_doc = account_profile_info.account_profile_obj || {};
|
|
5848
5877
|
const render_ret = await email_ms.render_profile_email({
|
|
5878
|
+
// The template and signature live on the attached email account now; the profile
|
|
5879
|
+
// fields below are the fallback for a profile configured before the Email hub.
|
|
5880
|
+
uid,
|
|
5881
|
+
email_account_id: profile_doc.email_account_id,
|
|
5849
5882
|
style: profile_doc.email_template?.style,
|
|
5850
5883
|
body_text: body,
|
|
5851
5884
|
profile_name: profile_doc.profile_name,
|
|
@@ -11346,6 +11379,15 @@ export const inspect_profile_picture = async function (req, job_id, headers) {
|
|
|
11346
11379
|
'snake_case code. Use ok when acceptable. Otherwise one of: not_a_person, is_a_logo, illustration, ai_generated, object_or_animal, screenshot, multiple_faces, face_not_visible, too_blurry, too_small, too_cropped, not_a_logo, no_clear_mark',
|
|
11347
11380
|
),
|
|
11348
11381
|
message: z.string().describe('one short, friendly sentence telling the user what to upload instead. No jargon, no mention of models or scores.'),
|
|
11382
|
+
// The user asked for a rejection to say WHY, specifically. A single
|
|
11383
|
+
// sentence is not enough to act on, so the model also itemises what it
|
|
11384
|
+
// actually saw and what would fix it.
|
|
11385
|
+
details: z
|
|
11386
|
+
.array(z.string())
|
|
11387
|
+
.describe(
|
|
11388
|
+
'When not acceptable, 1-3 short specific observations about THIS image explaining the rejection, e.g. "The image is a drawing, not a photograph" or "The face is cut off below the eyes". Describe what is actually in the image, never generic advice. Empty array when acceptable.',
|
|
11389
|
+
),
|
|
11390
|
+
how_to_fix: z.string().describe('When not acceptable, one short concrete instruction for the next attempt. Empty string when acceptable.'),
|
|
11349
11391
|
}),
|
|
11350
11392
|
metadata: { func: 'inspect_profile_picture', account_type },
|
|
11351
11393
|
account_profile_info,
|
|
@@ -11360,6 +11402,8 @@ export const inspect_profile_picture = async function (req, job_id, headers) {
|
|
|
11360
11402
|
acceptable: Boolean(res?.acceptable),
|
|
11361
11403
|
reason_code: res?.reason_code || (res?.acceptable ? 'ok' : 'unusable'),
|
|
11362
11404
|
message: res?.message || '',
|
|
11405
|
+
details: Array.isArray(res?.details) ? res.details.filter((d) => typeof d === 'string' && d.trim()).slice(0, 3) : [],
|
|
11406
|
+
how_to_fix: res?.how_to_fix || '',
|
|
11363
11407
|
account_type,
|
|
11364
11408
|
},
|
|
11365
11409
|
};
|
|
@@ -17430,11 +17474,12 @@ export const get_contact_form_bootstrap = async function (req) {
|
|
|
17430
17474
|
|
|
17431
17475
|
const canonical_id = `${account_profile_info.uid}.${account_profile_info.account_profile_id}`;
|
|
17432
17476
|
// Bot protection: surface the captcha site key when the owner requires the
|
|
17433
|
-
// "I'm not a robot" check on this profile's contact form.
|
|
17477
|
+
// "I'm not a robot" check on this profile's contact form. The engine lives
|
|
17478
|
+
// in bot_protection_module, this only asks it for the key.
|
|
17434
17479
|
let captcha_site_key = '';
|
|
17435
17480
|
try {
|
|
17436
|
-
const bp = await
|
|
17437
|
-
|
|
17481
|
+
const bp = await bot_ms.bp_contact_form_site_key({ uid: account_profile_info.uid, account_profile_id: account_profile_info.account_profile_id });
|
|
17482
|
+
captcha_site_key = bp?.data?.site_key || '';
|
|
17438
17483
|
} catch (e) {}
|
|
17439
17484
|
return {
|
|
17440
17485
|
code: 1,
|
|
@@ -17577,12 +17622,15 @@ export const contact_form_submit = async function (req, job_id, headers) {
|
|
|
17577
17622
|
|
|
17578
17623
|
// Bot protection: when the owner requires the "I'm not a robot" check on their
|
|
17579
17624
|
// contact form, verify the captcha response (single-use) before accepting.
|
|
17580
|
-
|
|
17581
|
-
try {
|
|
17582
|
-
|
|
17583
|
-
|
|
17584
|
-
|
|
17585
|
-
|
|
17625
|
+
// required:false comes back when the owner never asked for the check.
|
|
17626
|
+
try {
|
|
17627
|
+
const cr = await bot_ms.bp_contact_form_verify({
|
|
17628
|
+
uid: account_profile_info.uid,
|
|
17629
|
+
account_profile_id: account_profile_info.account_profile_id,
|
|
17630
|
+
response: req.captcha_response,
|
|
17631
|
+
});
|
|
17632
|
+
if (cr?.data?.required === true && cr?.data?.ok !== true) return { code: -401, data: 'captcha_failed' };
|
|
17633
|
+
} catch (e) {}
|
|
17586
17634
|
|
|
17587
17635
|
const owner_uid = account_profile_info.uid;
|
|
17588
17636
|
const config = _sanitize_contact_form_config(ap_doc.contact_form_config);
|
|
@@ -17723,6 +17771,10 @@ const contact_ticket_conversation = async function (req, job_id, headers) {
|
|
|
17723
17771
|
let html = null;
|
|
17724
17772
|
try {
|
|
17725
17773
|
const render_ret = await email_ms.render_profile_email({
|
|
17774
|
+
// The template and signature live on the attached email account now; the profile
|
|
17775
|
+
// fields below are the fallback for a profile configured before the Email hub.
|
|
17776
|
+
uid,
|
|
17777
|
+
email_account_id: profile_doc.email_account_id,
|
|
17726
17778
|
style: profile_doc.email_template?.style,
|
|
17727
17779
|
body_text: body,
|
|
17728
17780
|
profile_name: profile_doc.profile_name,
|
|
@@ -17794,910 +17846,3 @@ const contact_ticket_conversation = async function (req, job_id, headers) {
|
|
|
17794
17846
|
return { code: -15, data: err.message };
|
|
17795
17847
|
}
|
|
17796
17848
|
};
|
|
17797
|
-
|
|
17798
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
17799
|
-
// BOT PROTECTION ("I'm not a robot") - verification engine
|
|
17800
|
-
//
|
|
17801
|
-
// Escalation ladder: L0 checkbox + passive signals + proof of work -> L1 image
|
|
17802
|
-
// challenge (from-scratch SVG synthetic tiles) -> L2 AI check (score, then a
|
|
17803
|
-
// one-shot AI-generated challenge). Two surfaces consume it: the embeddable
|
|
17804
|
-
// widget (public site_key + server-side secret_key siteverify, reCAPTCHA shaped)
|
|
17805
|
-
// and the origin interstitial (router). This generalizes the contact-form
|
|
17806
|
-
// human-detection token above. Docs live in xuda_master so the router edge can
|
|
17807
|
-
// resolve them in every region. See docs/handoff_bot_protection_ishai.md.
|
|
17808
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
17809
|
-
|
|
17810
|
-
const BP_DB = 'xuda_master';
|
|
17811
|
-
const _bp_conf = () => (_conf && _conf.bot_protection) || {};
|
|
17812
|
-
const _bp_defaults = () => ({
|
|
17813
|
-
pow_difficulty: 16,
|
|
17814
|
-
session_token_ttl_ms: 900000,
|
|
17815
|
-
response_token_ttl_ms: 120000,
|
|
17816
|
-
clearance_ttl_ms: 1800000,
|
|
17817
|
-
min_age_ms: 800,
|
|
17818
|
-
image_grid: 9,
|
|
17819
|
-
image_correct_min: 2,
|
|
17820
|
-
aggressiveness: 'medium',
|
|
17821
|
-
l1_score_low: 0.35,
|
|
17822
|
-
l2_score_low: 0.7,
|
|
17823
|
-
...(_bp_conf().defaults || {}),
|
|
17824
|
-
});
|
|
17825
|
-
|
|
17826
|
-
// Fleet-shared HMAC key, same derivation style as the contact-form key so the
|
|
17827
|
-
// router and this module sign and verify the same tokens without distributing a
|
|
17828
|
-
// new secret.
|
|
17829
|
-
const _bot_hmac_key = function () {
|
|
17830
|
-
const seed = String(_conf?.gmail?.clientSecret || _conf?.domain || 'xuda');
|
|
17831
|
-
return crypto.createHash('sha256').update(`xuda_bot_protection_v1:${seed}`).digest();
|
|
17832
|
-
};
|
|
17833
|
-
const _bp_sig = (input) => crypto.createHmac('sha256', _bot_hmac_key()).update(String(input)).digest('hex').slice(0, 32);
|
|
17834
|
-
const _bp_rand = (n) => crypto.randomBytes(n).toString('base64url');
|
|
17835
|
-
const _bp_sha = (s) => crypto.createHash('sha256').update(String(s)).digest('hex');
|
|
17836
|
-
const _bp_aggr = (name) => ({ low: 0, medium: 1, high: 2 }[name] ?? 1);
|
|
17837
|
-
const _bp_pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
|
17838
|
-
|
|
17839
|
-
// --- Clearance / session tokens ------------------------------------------
|
|
17840
|
-
// Format: xcl1.<scope>.<ts>.<ttl>.<sig>. The subject is bound through the
|
|
17841
|
-
// signature, not stored in the token. scope in { session, response, clearance }.
|
|
17842
|
-
export const mint_clearance_token = function (scope, subject, ttl_ms, ts = Date.now()) {
|
|
17843
|
-
const sig = _bp_sig(`${scope}.${subject}.${ts}.${ttl_ms}`);
|
|
17844
|
-
return `xcl1.${scope}.${ts}.${ttl_ms}.${sig}`;
|
|
17845
|
-
};
|
|
17846
|
-
export const verify_clearance_token = function (scope, subject, token, min_age_ms = 0) {
|
|
17847
|
-
const p = String(token || '').split('.');
|
|
17848
|
-
if (p.length !== 5 || p[0] !== 'xcl1' || p[1] !== scope) return { ok: false, reason: 'bad_token' };
|
|
17849
|
-
const ts = Number(p[2]);
|
|
17850
|
-
const ttl = Number(p[3]);
|
|
17851
|
-
if (!Number.isFinite(ts) || !Number.isFinite(ttl)) return { ok: false, reason: 'bad_token' };
|
|
17852
|
-
const expected = _bp_sig(`${scope}.${subject}.${ts}.${ttl}`);
|
|
17853
|
-
let ok = false;
|
|
17854
|
-
try { ok = crypto.timingSafeEqual(Buffer.from(p[4]), Buffer.from(expected)); } catch (e) { ok = false; }
|
|
17855
|
-
if (!ok) return { ok: false, reason: 'bad_sig' };
|
|
17856
|
-
const age = Date.now() - ts;
|
|
17857
|
-
if (age > ttl) return { ok: false, reason: 'expired' };
|
|
17858
|
-
if (age < min_age_ms) return { ok: false, reason: 'too_fast' };
|
|
17859
|
-
return { ok: true, ts };
|
|
17860
|
-
};
|
|
17861
|
-
|
|
17862
|
-
// --- Response tokens (returned on PASS, verified by siteverify) -----------
|
|
17863
|
-
// Self-contained (survives a cross-process hop) and single-use (best-effort
|
|
17864
|
-
// per-process burn set, same posture as the contact-form throttles).
|
|
17865
|
-
const _bp_used_responses = new Map(); // jti -> expiry ts
|
|
17866
|
-
const _bp_burn = (jti, ttl) => {
|
|
17867
|
-
_bp_used_responses.set(jti, Date.now() + ttl);
|
|
17868
|
-
if (_bp_used_responses.size > 20000) {
|
|
17869
|
-
const now = Date.now();
|
|
17870
|
-
for (const [k, exp] of _bp_used_responses) if (exp < now) _bp_used_responses.delete(k);
|
|
17871
|
-
}
|
|
17872
|
-
};
|
|
17873
|
-
const _bp_is_burned = (jti) => {
|
|
17874
|
-
const exp = _bp_used_responses.get(jti);
|
|
17875
|
-
if (!exp) return false;
|
|
17876
|
-
if (exp < Date.now()) { _bp_used_responses.delete(jti); return false; }
|
|
17877
|
-
return true;
|
|
17878
|
-
};
|
|
17879
|
-
const mint_response_token = function (site_key, level, score, ttl_ms) {
|
|
17880
|
-
const ts = Date.now();
|
|
17881
|
-
const jti = _bp_rand(9);
|
|
17882
|
-
const score100 = Math.max(0, Math.min(100, Math.round((score || 0) * 100)));
|
|
17883
|
-
const sig = _bp_sig(`resp.${site_key}.${ts}.${ttl_ms}.${level}.${score100}.${jti}`);
|
|
17884
|
-
return `xrsp1.${ts}.${ttl_ms}.${level}.${score100}.${jti}.${sig}`;
|
|
17885
|
-
};
|
|
17886
|
-
const verify_response_token = function (site_key, token) {
|
|
17887
|
-
const p = String(token || '').split('.');
|
|
17888
|
-
if (p.length !== 7 || p[0] !== 'xrsp1') return { ok: false, reason: 'bad_token' };
|
|
17889
|
-
const [, ts_s, ttl_s, level_s, score_s, jti, sig] = p;
|
|
17890
|
-
const ts = Number(ts_s);
|
|
17891
|
-
const ttl = Number(ttl_s);
|
|
17892
|
-
if (!Number.isFinite(ts) || !Number.isFinite(ttl)) return { ok: false, reason: 'bad_token' };
|
|
17893
|
-
const expected = _bp_sig(`resp.${site_key}.${ts}.${ttl}.${level_s}.${score_s}.${jti}`);
|
|
17894
|
-
let ok = false;
|
|
17895
|
-
try { ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); } catch (e) { ok = false; }
|
|
17896
|
-
if (!ok) return { ok: false, reason: 'bad_sig' };
|
|
17897
|
-
if (Date.now() - ts > ttl) return { ok: false, reason: 'expired' };
|
|
17898
|
-
if (_bp_is_burned(jti)) return { ok: false, reason: 'replayed' };
|
|
17899
|
-
_bp_burn(jti, ttl);
|
|
17900
|
-
return { ok: true, level: Number(level_s), score: Number(score_s) / 100 };
|
|
17901
|
-
};
|
|
17902
|
-
|
|
17903
|
-
// --- Data model (docs in xuda_master) ------------------------------------
|
|
17904
|
-
const _bp_profile_id = (uid, slug) => `bot_protection:${uid}.${slug}`;
|
|
17905
|
-
const _bp_target_key = (doc) => (doc && doc.target && doc.target.id ? String(doc.target.id) : '');
|
|
17906
|
-
const _bp_index_id = {
|
|
17907
|
-
key: (k) => `bpk::${k}`,
|
|
17908
|
-
secret: (s) => `bps::${_bp_sha(s)}`,
|
|
17909
|
-
target: (t) => `bpt::${t}`,
|
|
17910
|
-
};
|
|
17911
|
-
const _bp_load = async (profile_id) => {
|
|
17912
|
-
const r = await db_module.get_couch_doc(BP_DB, profile_id, true);
|
|
17913
|
-
return r && r.code > 0 && r.data && r.data._id ? r.data : null;
|
|
17914
|
-
};
|
|
17915
|
-
const _bp_load_index = async (id) => {
|
|
17916
|
-
const r = await db_module.get_couch_doc(BP_DB, id, true);
|
|
17917
|
-
return r && r.code > 0 && r.data && r.data.profile_id ? r.data : null;
|
|
17918
|
-
};
|
|
17919
|
-
const _bp_write_index = async (id, profile_id) => {
|
|
17920
|
-
const existing = await _bp_load_index(id);
|
|
17921
|
-
const doc = existing
|
|
17922
|
-
? { ...existing, profile_id, ts: Date.now() }
|
|
17923
|
-
: { _id: id, docType: 'bot_protection_index', profile_id, ts: Date.now() };
|
|
17924
|
-
await db_module.save_couch_doc(BP_DB, doc);
|
|
17925
|
-
};
|
|
17926
|
-
const _bp_delete_index = async (id) => {
|
|
17927
|
-
const r = await db_module.get_couch_doc(BP_DB, id, true);
|
|
17928
|
-
if (r && r.code > 0 && r.data && r.data._rev) {
|
|
17929
|
-
try { await db_module.delete_couch_doc(BP_DB, id, r.data._rev); } catch (e) {}
|
|
17930
|
-
}
|
|
17931
|
-
};
|
|
17932
|
-
const _bp_resolve_by_site_key = async (site_key) => {
|
|
17933
|
-
const idx = await _bp_load_index(_bp_index_id.key(site_key));
|
|
17934
|
-
return idx ? await _bp_load(idx.profile_id) : null;
|
|
17935
|
-
};
|
|
17936
|
-
const _bp_resolve_by_secret = async (secret) => {
|
|
17937
|
-
const idx = await _bp_load_index(_bp_index_id.secret(secret));
|
|
17938
|
-
return idx ? await _bp_load(idx.profile_id) : null;
|
|
17939
|
-
};
|
|
17940
|
-
const _bp_new_keys = () => ({ site_key: _bp_rand(18), secret_key: _bp_rand(32) });
|
|
17941
|
-
|
|
17942
|
-
const _bp_default_escalation = () => {
|
|
17943
|
-
const d = _bp_defaults();
|
|
17944
|
-
return {
|
|
17945
|
-
pow_difficulty: d.pow_difficulty,
|
|
17946
|
-
aggressiveness: d.aggressiveness,
|
|
17947
|
-
clearance_ttl_ms: d.clearance_ttl_ms,
|
|
17948
|
-
response_token_ttl_ms: d.response_token_ttl_ms,
|
|
17949
|
-
session_token_ttl_ms: d.session_token_ttl_ms,
|
|
17950
|
-
l1_enabled: true,
|
|
17951
|
-
l2_enabled: true,
|
|
17952
|
-
};
|
|
17953
|
-
};
|
|
17954
|
-
const _bp_ensure_profile = async (uid, slug, seed = {}) => {
|
|
17955
|
-
const id = _bp_profile_id(uid, slug);
|
|
17956
|
-
let doc = await _bp_load(id);
|
|
17957
|
-
if (!doc) {
|
|
17958
|
-
const d = Date.now();
|
|
17959
|
-
doc = {
|
|
17960
|
-
_id: id,
|
|
17961
|
-
docType: 'bot_protection_profile',
|
|
17962
|
-
owner_uid: uid,
|
|
17963
|
-
enabled: false,
|
|
17964
|
-
surface: seed.surface || 'widget',
|
|
17965
|
-
plan_tier: seed.plan_tier || 'standard',
|
|
17966
|
-
site_key: '',
|
|
17967
|
-
secret_key: '',
|
|
17968
|
-
target: seed.target || null,
|
|
17969
|
-
escalation: { start_level: 0, ..._bp_default_escalation() },
|
|
17970
|
-
custom_rules: {},
|
|
17971
|
-
require_on_contact_form: false,
|
|
17972
|
-
armed: false,
|
|
17973
|
-
stats: { served: 0, passed: 0, challenged: 0, blocked: 0 },
|
|
17974
|
-
date_created_ts: d,
|
|
17975
|
-
ts: d,
|
|
17976
|
-
};
|
|
17977
|
-
}
|
|
17978
|
-
return doc;
|
|
17979
|
-
};
|
|
17980
|
-
const _bp_save = async (doc) => {
|
|
17981
|
-
doc.ts = Date.now();
|
|
17982
|
-
const ret = await db_module.save_couch_doc(BP_DB, doc);
|
|
17983
|
-
if (doc.site_key) await _bp_write_index(_bp_index_id.key(doc.site_key), doc._id);
|
|
17984
|
-
if (doc.secret_key) await _bp_write_index(_bp_index_id.secret(doc.secret_key), doc._id);
|
|
17985
|
-
const tkey = _bp_target_key(doc);
|
|
17986
|
-
if (tkey) await _bp_write_index(_bp_index_id.target(tkey), doc._id);
|
|
17987
|
-
return ret;
|
|
17988
|
-
};
|
|
17989
|
-
|
|
17990
|
-
// --- Stats (in-process buffer, persisted opportunistically) ---------------
|
|
17991
|
-
const _bp_stat_buf = new Map();
|
|
17992
|
-
const _bp_bump_stat = (profile_id, field, n = 1) => {
|
|
17993
|
-
const b = _bp_stat_buf.get(profile_id) || { served: 0, passed: 0, challenged: 0, blocked: 0 };
|
|
17994
|
-
b[field] = (b[field] || 0) + n;
|
|
17995
|
-
_bp_stat_buf.set(profile_id, b);
|
|
17996
|
-
};
|
|
17997
|
-
const _bp_merged_stats = (doc) => {
|
|
17998
|
-
const base = doc.stats || { served: 0, passed: 0, challenged: 0, blocked: 0 };
|
|
17999
|
-
const b = _bp_stat_buf.get(doc._id) || {};
|
|
18000
|
-
return {
|
|
18001
|
-
served: (base.served || 0) + (b.served || 0),
|
|
18002
|
-
passed: (base.passed || 0) + (b.passed || 0),
|
|
18003
|
-
challenged: (base.challenged || 0) + (b.challenged || 0),
|
|
18004
|
-
blocked: (base.blocked || 0) + (b.blocked || 0),
|
|
18005
|
-
};
|
|
18006
|
-
};
|
|
18007
|
-
const _bp_flush_stats = (doc) => {
|
|
18008
|
-
if (_bp_stat_buf.has(doc._id)) {
|
|
18009
|
-
doc.stats = _bp_merged_stats(doc);
|
|
18010
|
-
_bp_stat_buf.delete(doc._id);
|
|
18011
|
-
}
|
|
18012
|
-
return doc;
|
|
18013
|
-
};
|
|
18014
|
-
|
|
18015
|
-
// --- Rate limiter (per-process, flood-stopping) --------------------------
|
|
18016
|
-
const _bp_hits = { ip: {}, key: {} };
|
|
18017
|
-
const _bp_allow = function (bucket, key, max, window_ms) {
|
|
18018
|
-
const now = Date.now();
|
|
18019
|
-
const store = _bp_hits[bucket];
|
|
18020
|
-
store[key] = (store[key] || []).filter((t) => now - t < window_ms);
|
|
18021
|
-
if (store[key].length >= max) return false;
|
|
18022
|
-
store[key].push(now);
|
|
18023
|
-
if (Object.keys(store).length > 5000) {
|
|
18024
|
-
for (const k of Object.keys(store)) {
|
|
18025
|
-
if (!store[k].length || now - store[k][store[k].length - 1] > window_ms) delete store[k];
|
|
18026
|
-
}
|
|
18027
|
-
}
|
|
18028
|
-
return true;
|
|
18029
|
-
};
|
|
18030
|
-
|
|
18031
|
-
// --- Proof of work -------------------------------------------------------
|
|
18032
|
-
const _leading_zero_bits = (hex) => {
|
|
18033
|
-
let bits = 0;
|
|
18034
|
-
for (const ch of hex) {
|
|
18035
|
-
const v = parseInt(ch, 16);
|
|
18036
|
-
if (v === 0) { bits += 4; continue; }
|
|
18037
|
-
if (v < 2) bits += 3;
|
|
18038
|
-
else if (v < 4) bits += 2;
|
|
18039
|
-
else if (v < 8) bits += 1;
|
|
18040
|
-
break;
|
|
18041
|
-
}
|
|
18042
|
-
return bits;
|
|
18043
|
-
};
|
|
18044
|
-
const _pow_prefix = (session_token) => _bp_sig(`pow.${session_token}`).slice(0, 16);
|
|
18045
|
-
const pow_make = (session_token, difficulty) => ({ prefix: _pow_prefix(session_token), difficulty });
|
|
18046
|
-
const pow_check = (session_token, difficulty, nonce) => {
|
|
18047
|
-
if (nonce == null) return false;
|
|
18048
|
-
const h = crypto.createHash('sha256').update(`${_pow_prefix(session_token)}.${nonce}`).digest('hex');
|
|
18049
|
-
return _leading_zero_bits(h) >= difficulty;
|
|
18050
|
-
};
|
|
18051
|
-
const _bp_pow_difficulty = (profile) => {
|
|
18052
|
-
const d = _bp_defaults();
|
|
18053
|
-
const base = Number(profile?.escalation?.pow_difficulty || d.pow_difficulty);
|
|
18054
|
-
const bump = _bp_aggr(profile?.escalation?.aggressiveness || d.aggressiveness);
|
|
18055
|
-
return Math.max(8, base + bump);
|
|
18056
|
-
};
|
|
18057
|
-
|
|
18058
|
-
// --- L0 signal scoring ---------------------------------------------------
|
|
18059
|
-
const _bp_client_ip = (req = {}, headers = {}) => {
|
|
18060
|
-
const h = headers || {};
|
|
18061
|
-
const xff = h['x-forwarded-for'] || h['X-Forwarded-For'] || req.remoteip || req.ip || '';
|
|
18062
|
-
return String(xff).split(',')[0].trim();
|
|
18063
|
-
};
|
|
18064
|
-
const _bp_safe_signals = (s = {}) => {
|
|
18065
|
-
const o = {};
|
|
18066
|
-
for (const k of ['pointer_events', 'key_events', 'scroll_events', 'has_touch', 'pointer_moved', 'webgl', 'canvas', 'hardware_concurrency', 'languages', 'timezone', 'webdriver', 'headless', 'dwell_ms', 'user_agent', 'screen']) {
|
|
18067
|
-
if (s[k] !== undefined) o[k] = typeof s[k] === 'string' ? String(s[k]).slice(0, 120) : s[k];
|
|
18068
|
-
}
|
|
18069
|
-
return o;
|
|
18070
|
-
};
|
|
18071
|
-
const _bp_score_signals = (signals = {}) => {
|
|
18072
|
-
const s = signals || {};
|
|
18073
|
-
let score = 0.5;
|
|
18074
|
-
const reasons = [];
|
|
18075
|
-
const inter = Number(s.pointer_events || 0) + Number(s.key_events || 0) + Number(s.scroll_events || 0);
|
|
18076
|
-
if (inter >= 3) score += 0.25;
|
|
18077
|
-
else if (inter === 0) { score -= 0.2; reasons.push('no_interaction'); }
|
|
18078
|
-
if (s.has_touch === true || s.pointer_moved === true) score += 0.05;
|
|
18079
|
-
if (s.webgl && s.canvas) score += 0.1;
|
|
18080
|
-
else { score -= 0.1; reasons.push('no_env_hash'); }
|
|
18081
|
-
if (s.hardware_concurrency && Number(s.hardware_concurrency) > 0) score += 0.03;
|
|
18082
|
-
if (s.languages && String(s.languages).length) score += 0.03;
|
|
18083
|
-
else reasons.push('no_lang');
|
|
18084
|
-
if (s.timezone) score += 0.02;
|
|
18085
|
-
if (s.webdriver === true) { score -= 0.5; reasons.push('webdriver'); }
|
|
18086
|
-
if (s.headless === true) { score -= 0.4; reasons.push('headless'); }
|
|
18087
|
-
const dwell = Number(s.dwell_ms || 0);
|
|
18088
|
-
if (dwell >= 400) score += 0.05;
|
|
18089
|
-
else if (dwell > 0 && dwell < 120) { score -= 0.1; reasons.push('instant_click'); }
|
|
18090
|
-
score = Math.max(0, Math.min(1, score));
|
|
18091
|
-
return { score, reasons };
|
|
18092
|
-
};
|
|
18093
|
-
|
|
18094
|
-
// --- L1 image challenge (from-scratch synthetic SVG tiles) ----------------
|
|
18095
|
-
const _BP_SHAPES = ['circle', 'square', 'triangle', 'star', 'hexagon'];
|
|
18096
|
-
const _bp_color = () => _bp_pick(['#e11d48', '#2563eb', '#16a34a', '#eab308', '#7c3aed', '#ea580c', '#0891b2', '#db2777']);
|
|
18097
|
-
const _bp_shape_svg = (kind, color) => {
|
|
18098
|
-
switch (kind) {
|
|
18099
|
-
case 'circle': return `<circle cx="50" cy="50" r="30" fill="${color}"/>`;
|
|
18100
|
-
case 'square': return `<rect x="22" y="22" width="56" height="56" rx="6" fill="${color}"/>`;
|
|
18101
|
-
case 'triangle': return `<polygon points="50,18 82,80 18,80" fill="${color}"/>`;
|
|
18102
|
-
case 'star': return `<polygon points="50,15 61,40 88,40 66,57 74,84 50,68 26,84 34,57 12,40 39,40" fill="${color}"/>`;
|
|
18103
|
-
case 'hexagon': return `<polygon points="50,16 82,33 82,67 50,84 18,67 18,33" fill="${color}"/>`;
|
|
18104
|
-
default: return '';
|
|
18105
|
-
}
|
|
18106
|
-
};
|
|
18107
|
-
const _bp_tile = (kind) => {
|
|
18108
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" fill="#f1f5f9"/>${_bp_shape_svg(kind, _bp_color())}</svg>`;
|
|
18109
|
-
return 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
|
18110
|
-
};
|
|
18111
|
-
const _image_issue = (site_key, d) => {
|
|
18112
|
-
const grid = d.image_grid || 9;
|
|
18113
|
-
const target = _bp_pick(_BP_SHAPES);
|
|
18114
|
-
const others = _BP_SHAPES.filter((x) => x !== target);
|
|
18115
|
-
const min_c = Math.max(1, d.image_correct_min || 2);
|
|
18116
|
-
const max_c = Math.max(min_c, grid - 2);
|
|
18117
|
-
const correct_count = min_c + Math.floor(Math.random() * (max_c - min_c + 1));
|
|
18118
|
-
const order = [...Array(grid).keys()];
|
|
18119
|
-
for (let i = order.length - 1; i > 0; i--) {
|
|
18120
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
18121
|
-
[order[i], order[j]] = [order[j], order[i]];
|
|
18122
|
-
}
|
|
18123
|
-
const correct = new Set(order.slice(0, correct_count));
|
|
18124
|
-
const tiles = [];
|
|
18125
|
-
const answer = [];
|
|
18126
|
-
for (let i = 0; i < grid; i++) {
|
|
18127
|
-
if (correct.has(i)) { tiles.push(_bp_tile(target)); answer.push(i); }
|
|
18128
|
-
else tiles.push(_bp_tile(_bp_pick(others)));
|
|
18129
|
-
}
|
|
18130
|
-
answer.sort((a, b) => a - b);
|
|
18131
|
-
return {
|
|
18132
|
-
id: _bp_rand(9), kind: 'image', level: 1, site_key, ts: Date.now(), ttl: 120000,
|
|
18133
|
-
grid, tiles, prompt: `Select every image that shows a ${target}`,
|
|
18134
|
-
answer_hash: _bp_sha(answer.join(',')), pass_score: 0.85,
|
|
18135
|
-
};
|
|
18136
|
-
};
|
|
18137
|
-
const _image_grade = (ch, answer) => {
|
|
18138
|
-
const raw = Array.isArray(answer) ? answer : answer && Array.isArray(answer.selection) ? answer.selection : [];
|
|
18139
|
-
const norm = [...new Set(raw.map(Number).filter((n) => Number.isInteger(n) && n >= 0 && n < ch.grid))].sort((a, b) => a - b).join(',');
|
|
18140
|
-
return _bp_sha(norm) === ch.answer_hash;
|
|
18141
|
-
};
|
|
18142
|
-
|
|
18143
|
-
// --- L2 AI check (score, then a one-shot AI challenge) --------------------
|
|
18144
|
-
const _bp_ip_reputation = async (ip) => {
|
|
18145
|
-
const s = String(ip || '');
|
|
18146
|
-
const is_private = /^(10\.|127\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|::1|fc|fd)/.test(s);
|
|
18147
|
-
return { ip: s, private: is_private };
|
|
18148
|
-
};
|
|
18149
|
-
const _RiskSchema = z.object({ score: z.number(), verdict: z.enum(['human', 'bot', 'borderline']), reason: z.string() });
|
|
18150
|
-
const _risk_score = async (profile, signals, ip) => {
|
|
18151
|
-
try {
|
|
18152
|
-
const rep = await _bp_ip_reputation(ip);
|
|
18153
|
-
const prompt = `You are a bot-detection risk scorer for a website captcha. Given browser signals and IP reputation, decide if the visitor is a human, a bot, or borderline. score is a 0..1 human-likelihood.\nSIGNALS: ${JSON.stringify(_bp_safe_signals(signals))}\nIP_REPUTATION: ${JSON.stringify(rep)}`;
|
|
18154
|
-
const r = await submit_chat_gpt_prompt({ uid: profile.owner_uid, model: _conf.default_ai_model, prompt, response_format: _RiskSchema, metadata: { func: 'bot_risk', profile_id: profile._id } });
|
|
18155
|
-
if (r.code === 5 && r.data) {
|
|
18156
|
-
const o = JSON.parse(r.data);
|
|
18157
|
-
return { score: Number(o.score) || 0, verdict: o.verdict || 'borderline', reason: o.reason || '' };
|
|
18158
|
-
}
|
|
18159
|
-
} catch (e) {}
|
|
18160
|
-
return { score: 0.5, verdict: 'borderline', reason: 'scorer_unavailable' };
|
|
18161
|
-
};
|
|
18162
|
-
const _bp_norm_answer = (s) => String(s || '').toLowerCase().trim().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
18163
|
-
const _DynSchema = z.object({ question: z.string(), answer: z.string() });
|
|
18164
|
-
const _dynamic_issue = async (site_key, owner_uid) => {
|
|
18165
|
-
try {
|
|
18166
|
-
const prompt = 'Generate ONE short human-verification question that is easy for a person and hard for a naive bot script. Prefer a simple common-sense or arithmetic-in-words question with a single short unambiguous answer. Return the question and the exact expected answer (a single lowercase word or number, no punctuation).';
|
|
18167
|
-
const r = await submit_chat_gpt_prompt({ uid: owner_uid || 'bot_protection', model: _conf.default_ai_model, prompt, response_format: _DynSchema, metadata: { func: 'bot_dyn' } });
|
|
18168
|
-
if (r.code === 5 && r.data) {
|
|
18169
|
-
const o = JSON.parse(r.data);
|
|
18170
|
-
if (o.question && o.answer) {
|
|
18171
|
-
return { id: _bp_rand(9), kind: 'ai', level: 2, site_key, ts: Date.now(), ttl: 180000, prompt: String(o.question).slice(0, 300), answer_hash: _bp_sha(_bp_norm_answer(o.answer)), pass_score: 0.9 };
|
|
18172
|
-
}
|
|
18173
|
-
}
|
|
18174
|
-
} catch (e) {}
|
|
18175
|
-
return null;
|
|
18176
|
-
};
|
|
18177
|
-
const _dynamic_grade = async (ch, answer) => {
|
|
18178
|
-
const a = typeof answer === 'string' ? answer : (answer && (answer.text || answer.answer)) || '';
|
|
18179
|
-
return _bp_sha(_bp_norm_answer(a)) === ch.answer_hash;
|
|
18180
|
-
};
|
|
18181
|
-
|
|
18182
|
-
// --- Escalation orchestrator ---------------------------------------------
|
|
18183
|
-
const _bp_challenges = new Map(); // challenge_id -> { kind, level, site_key, ts, ttl, answer_hash, ... }
|
|
18184
|
-
const _bp_escalate = async (profile, site_key, ip, signals, level, esc, d) => {
|
|
18185
|
-
if (level <= 1 && esc.l1_enabled !== false) {
|
|
18186
|
-
const ch = _image_issue(site_key, d);
|
|
18187
|
-
_bp_challenges.set(ch.id, ch);
|
|
18188
|
-
_bp_bump_stat(profile._id, 'challenged');
|
|
18189
|
-
return { code: 1, data: { status: 'challenge', level: 1, challenge: { id: ch.id, type: 'image', prompt: ch.prompt, tiles: ch.tiles, grid: ch.grid } } };
|
|
18190
|
-
}
|
|
18191
|
-
if (level <= 2 && esc.l2_enabled !== false) {
|
|
18192
|
-
const verdict = await _risk_score(profile, signals, ip);
|
|
18193
|
-
if (verdict.verdict === 'human' && verdict.score >= (d.l2_score_low || 0.7)) {
|
|
18194
|
-
_bp_bump_stat(profile._id, 'passed');
|
|
18195
|
-
return { code: 1, data: { status: 'pass', level: 2, response_token: mint_response_token(site_key, 2, verdict.score, esc.response_token_ttl_ms || d.response_token_ttl_ms) } };
|
|
18196
|
-
}
|
|
18197
|
-
if (verdict.verdict === 'bot') {
|
|
18198
|
-
_bp_bump_stat(profile._id, 'blocked');
|
|
18199
|
-
return { code: 1, data: { status: 'fail', level: 2, reason: 'ai_bot' } };
|
|
18200
|
-
}
|
|
18201
|
-
const ch = await _dynamic_issue(site_key, profile.owner_uid);
|
|
18202
|
-
if (ch) {
|
|
18203
|
-
_bp_challenges.set(ch.id, ch);
|
|
18204
|
-
_bp_bump_stat(profile._id, 'challenged');
|
|
18205
|
-
return { code: 1, data: { status: 'challenge', level: 2, challenge: { id: ch.id, type: 'ai', prompt: ch.prompt } } };
|
|
18206
|
-
}
|
|
18207
|
-
_bp_bump_stat(profile._id, 'blocked');
|
|
18208
|
-
return { code: 1, data: { status: 'fail', level: 2, reason: 'ai_uncertain' } };
|
|
18209
|
-
}
|
|
18210
|
-
_bp_bump_stat(profile._id, 'blocked');
|
|
18211
|
-
return { code: 1, data: { status: 'fail', level, reason: 'exhausted' } };
|
|
18212
|
-
};
|
|
18213
|
-
|
|
18214
|
-
// --- Bootstrap (broker-only; called by the router iframe + interstitial) --
|
|
18215
|
-
export const get_captcha_bootstrap = async function (req) {
|
|
18216
|
-
try {
|
|
18217
|
-
const site_key = req.site_key;
|
|
18218
|
-
const ip = _bp_client_ip(req, req.headers);
|
|
18219
|
-
if (!site_key) return { code: -404, data: 'no_site_key' };
|
|
18220
|
-
const profile = await _bp_resolve_by_site_key(site_key);
|
|
18221
|
-
if (!profile || profile.enabled !== true) return { code: -404, data: 'not_enabled' };
|
|
18222
|
-
const d = _bp_defaults();
|
|
18223
|
-
const esc = profile.escalation || _bp_default_escalation();
|
|
18224
|
-
const session_token = mint_clearance_token('session', `${site_key}|${ip}`, esc.session_token_ttl_ms || d.session_token_ttl_ms);
|
|
18225
|
-
_bp_bump_stat(profile._id, 'served');
|
|
18226
|
-
return {
|
|
18227
|
-
code: 1,
|
|
18228
|
-
data: {
|
|
18229
|
-
site_key,
|
|
18230
|
-
session_token,
|
|
18231
|
-
pow: pow_make(session_token, _bp_pow_difficulty(profile)),
|
|
18232
|
-
min_age_ms: d.min_age_ms,
|
|
18233
|
-
config: {
|
|
18234
|
-
brand: profile.brand || 'Xuda',
|
|
18235
|
-
theme: req.theme || profile.theme || 'auto',
|
|
18236
|
-
l1_enabled: esc.l1_enabled !== false,
|
|
18237
|
-
l2_enabled: esc.l2_enabled !== false,
|
|
18238
|
-
},
|
|
18239
|
-
},
|
|
18240
|
-
};
|
|
18241
|
-
} catch (err) {
|
|
18242
|
-
return { code: -404, data: err.message || String(err) };
|
|
18243
|
-
}
|
|
18244
|
-
};
|
|
18245
|
-
|
|
18246
|
-
// --- Public: the escalation driver ---------------------------------------
|
|
18247
|
-
export const captcha_verify = async function (req, job_id, headers) {
|
|
18248
|
-
try {
|
|
18249
|
-
const { site_key, session_token, signals = {}, pow_nonce, challenge_id, answer } = req;
|
|
18250
|
-
const ip = _bp_client_ip(req, headers);
|
|
18251
|
-
if (!site_key || !session_token) return { code: 1, data: { status: 'fail', reason: 'bad_request' } };
|
|
18252
|
-
const profile = await _bp_resolve_by_site_key(site_key);
|
|
18253
|
-
if (!profile || profile.enabled !== true) return { code: 1, data: { status: 'fail', reason: 'not_enabled' } };
|
|
18254
|
-
const d = _bp_defaults();
|
|
18255
|
-
const esc = profile.escalation || _bp_default_escalation();
|
|
18256
|
-
|
|
18257
|
-
if (!_bp_allow('ip', `${site_key}|${ip}`, 60, 60000) || !_bp_allow('key', site_key, 600, 60000)) {
|
|
18258
|
-
return { code: 1, data: { status: 'fail', reason: 'rate_limited' } };
|
|
18259
|
-
}
|
|
18260
|
-
const st = verify_clearance_token('session', `${site_key}|${ip}`, session_token);
|
|
18261
|
-
if (!st.ok) return { code: 1, data: { status: 'fail', reason: `session_${st.reason}` } };
|
|
18262
|
-
|
|
18263
|
-
// Answering an issued image (L1) or AI (L2) challenge.
|
|
18264
|
-
if (challenge_id) {
|
|
18265
|
-
const ch = _bp_challenges.get(challenge_id);
|
|
18266
|
-
if (!ch || ch.site_key !== site_key || Date.now() - ch.ts > (ch.ttl || 120000)) {
|
|
18267
|
-
if (ch) _bp_challenges.delete(challenge_id);
|
|
18268
|
-
return { code: 1, data: { status: 'fail', reason: 'challenge_expired' } };
|
|
18269
|
-
}
|
|
18270
|
-
let ok = false;
|
|
18271
|
-
if (ch.kind === 'image') ok = _image_grade(ch, answer);
|
|
18272
|
-
else if (ch.kind === 'ai') ok = await _dynamic_grade(ch, answer);
|
|
18273
|
-
_bp_challenges.delete(challenge_id);
|
|
18274
|
-
if (ok) {
|
|
18275
|
-
_bp_bump_stat(profile._id, 'passed');
|
|
18276
|
-
return { code: 1, data: { status: 'pass', level: ch.level, response_token: mint_response_token(site_key, ch.level, ch.pass_score || 0.9, esc.response_token_ttl_ms || d.response_token_ttl_ms) } };
|
|
18277
|
-
}
|
|
18278
|
-
return await _bp_escalate(profile, site_key, ip, signals, ch.level + 1, esc, d);
|
|
18279
|
-
}
|
|
18280
|
-
|
|
18281
|
-
// Level 0: honeypot + min-age + proof of work + passive signals.
|
|
18282
|
-
if (signals && typeof signals.honeypot === 'string' && signals.honeypot.trim() !== '') {
|
|
18283
|
-
return { code: 1, data: { status: 'fail', reason: 'ok' } };
|
|
18284
|
-
}
|
|
18285
|
-
if (!pow_check(session_token, _bp_pow_difficulty(profile), pow_nonce)) {
|
|
18286
|
-
return { code: 1, data: { status: 'fail', reason: 'pow' } };
|
|
18287
|
-
}
|
|
18288
|
-
const min_age_ok = Date.now() - st.ts >= (d.min_age_ms || 800);
|
|
18289
|
-
const { score, reasons } = _bp_score_signals(signals);
|
|
18290
|
-
const aggr = _bp_aggr(esc.aggressiveness || d.aggressiveness);
|
|
18291
|
-
if (reasons.includes('webdriver') && aggr >= 2) return { code: 1, data: { status: 'fail', reason: 'automation' } };
|
|
18292
|
-
const pass_threshold = 0.6 + aggr * 0.1;
|
|
18293
|
-
if (min_age_ok && score >= pass_threshold) {
|
|
18294
|
-
_bp_bump_stat(profile._id, 'passed');
|
|
18295
|
-
return { code: 1, data: { status: 'pass', level: 0, response_token: mint_response_token(site_key, 0, score, esc.response_token_ttl_ms || d.response_token_ttl_ms) } };
|
|
18296
|
-
}
|
|
18297
|
-
return await _bp_escalate(profile, site_key, ip, signals, 1, esc, d);
|
|
18298
|
-
} catch (err) {
|
|
18299
|
-
return { code: 1, data: { status: 'fail', reason: 'error' } };
|
|
18300
|
-
}
|
|
18301
|
-
};
|
|
18302
|
-
|
|
18303
|
-
// --- Public: server-to-server verification (reCAPTCHA / Turnstile shaped) --
|
|
18304
|
-
export const captcha_siteverify = async function (req) {
|
|
18305
|
-
try {
|
|
18306
|
-
const { secret, response } = req;
|
|
18307
|
-
if (!secret || !response) return { code: 1, data: { success: false, 'error-codes': ['missing-input'] } };
|
|
18308
|
-
const profile = await _bp_resolve_by_secret(secret);
|
|
18309
|
-
if (!profile) return { code: 1, data: { success: false, 'error-codes': ['invalid-input-secret'] } };
|
|
18310
|
-
const v = verify_response_token(profile.site_key, response);
|
|
18311
|
-
if (!v.ok) return { code: 1, data: { success: false, 'error-codes': [v.reason === 'replayed' ? 'timeout-or-duplicate' : 'invalid-input-response'] } };
|
|
18312
|
-
return { code: 1, data: { success: true, score: v.score, level_reached: v.level, hostname: profile.target?.label || '', challenge_ts: Date.now(), 'error-codes': [] } };
|
|
18313
|
-
} catch (err) {
|
|
18314
|
-
return { code: 1, data: { success: false, 'error-codes': ['internal-error'] } };
|
|
18315
|
-
}
|
|
18316
|
-
};
|
|
18317
|
-
|
|
18318
|
-
// --- Entitlement (member-first; the Stripe attach lands in stripe_module) --
|
|
18319
|
-
// Entitlement gate. Standard service is FREE for paid members; non-members pay
|
|
18320
|
-
// $1/mo. Custom rules are a $5/mo add-on for EVERYONE (members included). Both
|
|
18321
|
-
// tiers share the consolidated-subscription category 'bot_protection' (one line
|
|
18322
|
-
// item, price-swapped between $1 and $5, exactly like ai_workspace tiers), so a
|
|
18323
|
-
// custom subscriber is never double-billed the standard price. On first use we
|
|
18324
|
-
// attach/swap the item via stripe_module; it returns -402 gracefully until the
|
|
18325
|
-
// live Stripe prices replace the config placeholders, so nothing hard-fails.
|
|
18326
|
-
const _bp_ensure_entitled = async (uid, tier) => {
|
|
18327
|
-
try {
|
|
18328
|
-
const r = await db_module.get_couch_doc('xuda_accounts', uid, true);
|
|
18329
|
-
const acct = r && r.code > 0 && r.data ? r.data : {};
|
|
18330
|
-
const is_member = !!(acct.membership_plan && acct.membership_plan !== 'free');
|
|
18331
|
-
const has_item = !!acct?.stripe_subscription_items?.bot_protection;
|
|
18332
|
-
if (tier !== 'custom') {
|
|
18333
|
-
if (is_member) return { ok: true, member: true };
|
|
18334
|
-
if (has_item) return { ok: true };
|
|
18335
|
-
}
|
|
18336
|
-
// Not entitled yet: attach the add-on (or swap the existing item to the
|
|
18337
|
-
// custom price) on the account's consolidated subscription.
|
|
18338
|
-
const plan_id = tier === 'custom' ? 'bot_protection_custom' : 'bot_protection';
|
|
18339
|
-
let attach = null;
|
|
18340
|
-
try { attach = await stripe_ms.add_subscription_item({ uid, plan_id }); } catch (e) {}
|
|
18341
|
-
if (attach && attach.code === 1) return { ok: true };
|
|
18342
|
-
return { ok: false, tier, price: tier === 'custom' ? 5 : 1, reason: attach && attach.data };
|
|
18343
|
-
} catch (e) {
|
|
18344
|
-
return { ok: false, tier, price: tier === 'custom' ? 5 : 1 };
|
|
18345
|
-
}
|
|
18346
|
-
};
|
|
18347
|
-
|
|
18348
|
-
// --- Config sanitizers ----------------------------------------------------
|
|
18349
|
-
const _bp_sanitize_escalation = (e) => {
|
|
18350
|
-
const out = {};
|
|
18351
|
-
if (['low', 'medium', 'high'].includes(e.aggressiveness)) out.aggressiveness = e.aggressiveness;
|
|
18352
|
-
if (Number.isFinite(Number(e.pow_difficulty))) out.pow_difficulty = Math.max(8, Math.min(24, Math.round(Number(e.pow_difficulty))));
|
|
18353
|
-
if (Number.isFinite(Number(e.clearance_ttl_ms))) out.clearance_ttl_ms = Math.max(60000, Math.min(86400000, Math.round(Number(e.clearance_ttl_ms))));
|
|
18354
|
-
if (typeof e.l1_enabled === 'boolean') out.l1_enabled = e.l1_enabled;
|
|
18355
|
-
if (typeof e.l2_enabled === 'boolean') out.l2_enabled = e.l2_enabled;
|
|
18356
|
-
return out;
|
|
18357
|
-
};
|
|
18358
|
-
const _bp_str_arr = (v, max = 200) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string').map((x) => x.slice(0, 80)).slice(0, max) : []);
|
|
18359
|
-
export const bp_sanitize_rules = function (rules = {}) {
|
|
18360
|
-
const r = rules || {};
|
|
18361
|
-
const out = {};
|
|
18362
|
-
if (r.geo && typeof r.geo === 'object') {
|
|
18363
|
-
out.geo = {
|
|
18364
|
-
mode: ['off', 'challenge', 'block', 'allow'].includes(r.geo.mode) ? r.geo.mode : 'off',
|
|
18365
|
-
countries: _bp_str_arr(r.geo.countries),
|
|
18366
|
-
regions: _bp_str_arr(r.geo.regions),
|
|
18367
|
-
};
|
|
18368
|
-
}
|
|
18369
|
-
const win = (o) => (o && typeof o === 'object' && ['off', 'challenge'].includes(o.mode) ? { mode: o.mode, list: (Array.isArray(o.list) ? o.list.map(Number).filter((n) => Number.isInteger(n)) : []) } : undefined);
|
|
18370
|
-
if (win(r.hours)) out.hours = win(r.hours);
|
|
18371
|
-
if (win(r.days)) out.days = win(r.days);
|
|
18372
|
-
if (Array.isArray(r.paths)) out.paths = r.paths.filter((p) => p && typeof p.pattern === 'string').map((p) => ({ pattern: p.pattern.slice(0, 200), action: ['challenge', 'block', 'allow'].includes(p.action) ? p.action : 'challenge' })).slice(0, 100);
|
|
18373
|
-
if (r.rate && typeof r.rate === 'object') out.rate = { window_s: Math.max(1, Math.min(3600, Number(r.rate.window_s) || 60)), max: Math.max(1, Math.min(100000, Number(r.rate.max) || 120)), action: ['challenge', 'block'].includes(r.rate.action) ? r.rate.action : 'challenge' };
|
|
18374
|
-
if (r.reputation && typeof r.reputation === 'object') out.reputation = { allow_verified_search_bots: r.reputation.allow_verified_search_bots !== false, challenge_bad_asn: r.reputation.challenge_bad_asn === true };
|
|
18375
|
-
out.allow_list = _bp_str_arr(r.allow_list);
|
|
18376
|
-
out.block_list = _bp_str_arr(r.block_list);
|
|
18377
|
-
if (typeof r.require_referer === 'boolean') out.require_referer = r.require_referer;
|
|
18378
|
-
if (Array.isArray(r.fingerprint_rules)) out.fingerprint_rules = r.fingerprint_rules.filter((f) => f && typeof f.match === 'string').map((f) => ({ match: f.match.slice(0, 40), action: ['challenge', 'block'].includes(f.action) ? f.action : 'challenge' })).slice(0, 20);
|
|
18379
|
-
if (['low', 'medium', 'high'].includes(r.aggressiveness)) out.aggressiveness = r.aggressiveness;
|
|
18380
|
-
if (Number.isFinite(Number(r.clearance_ttl_ms))) out.clearance_ttl_ms = Math.max(60000, Math.min(86400000, Math.round(Number(r.clearance_ttl_ms))));
|
|
18381
|
-
return out;
|
|
18382
|
-
};
|
|
18383
|
-
|
|
18384
|
-
// --- Owner settings (profile setup Bot Protection tab) -------------------
|
|
18385
|
-
const _build_captcha_snippet = (site_key, config = {}) => {
|
|
18386
|
-
const origin = embed_origin();
|
|
18387
|
-
const theme = config.theme && /^[a-z]+$/.test(config.theme) ? ` data-theme="${config.theme}"` : '';
|
|
18388
|
-
return `<script src="${origin}/dist/runtime/js/captcha-loader.js" async data-sitekey="${site_key}"${theme}></script>`;
|
|
18389
|
-
};
|
|
18390
|
-
const _build_captcha_container = (site_key) => `<div class="xuda-captcha" data-sitekey="${site_key}"></div>`;
|
|
18391
|
-
|
|
18392
|
-
export const get_bot_protection_settings = async function (req) {
|
|
18393
|
-
try {
|
|
18394
|
-
const { uid, profile_id } = req;
|
|
18395
|
-
const info = await get_active_account_profile_info(uid, profile_id);
|
|
18396
|
-
if (info.uid !== uid) return { code: -403, data: 'not_profile_owner' };
|
|
18397
|
-
const bp_id = _bp_profile_id(uid, info.account_profile_id);
|
|
18398
|
-
const doc = await _bp_load(bp_id);
|
|
18399
|
-
const site_key = doc?.site_key || '';
|
|
18400
|
-
const esc = doc?.escalation || _bp_default_escalation();
|
|
18401
|
-
return {
|
|
18402
|
-
code: 1,
|
|
18403
|
-
data: {
|
|
18404
|
-
enabled: doc?.enabled === true,
|
|
18405
|
-
plan_tier: doc?.plan_tier || 'standard',
|
|
18406
|
-
site_key,
|
|
18407
|
-
secret_key: doc?.secret_key || '',
|
|
18408
|
-
escalation: {
|
|
18409
|
-
aggressiveness: esc.aggressiveness,
|
|
18410
|
-
pow_difficulty: esc.pow_difficulty,
|
|
18411
|
-
clearance_ttl_ms: esc.clearance_ttl_ms,
|
|
18412
|
-
l1_enabled: esc.l1_enabled !== false,
|
|
18413
|
-
l2_enabled: esc.l2_enabled !== false,
|
|
18414
|
-
},
|
|
18415
|
-
custom_rules: doc?.custom_rules || {},
|
|
18416
|
-
require_on_contact_form: doc?.require_on_contact_form === true,
|
|
18417
|
-
widget_profile_id: `${uid}.${info.account_profile_id}`,
|
|
18418
|
-
iframe_url: site_key ? `${embed_origin()}/captcha/${site_key}` : '',
|
|
18419
|
-
snippet: site_key ? _build_captcha_snippet(site_key, { theme: doc?.theme }) : '',
|
|
18420
|
-
container_snippet: site_key ? _build_captcha_container(site_key) : '',
|
|
18421
|
-
},
|
|
18422
|
-
};
|
|
18423
|
-
} catch (err) {
|
|
18424
|
-
return { code: -1, data: err.message || String(err) };
|
|
18425
|
-
}
|
|
18426
|
-
};
|
|
18427
|
-
|
|
18428
|
-
export const update_bot_protection_settings = async function (req) {
|
|
18429
|
-
try {
|
|
18430
|
-
const { uid, profile_id, enabled, escalation, custom_rules, require_on_contact_form, rotate_secret } = req;
|
|
18431
|
-
const info = await get_active_account_profile_info(uid, profile_id);
|
|
18432
|
-
if (info.uid !== uid) return { code: -403, data: 'not_profile_owner' };
|
|
18433
|
-
const doc = await _bp_ensure_profile(uid, info.account_profile_id, { surface: 'widget' });
|
|
18434
|
-
|
|
18435
|
-
if (enabled === true && doc.enabled !== true) {
|
|
18436
|
-
const ent = await _bp_ensure_entitled(uid, doc.plan_tier || 'standard');
|
|
18437
|
-
if (!ent.ok) return { code: -402, data: { error: 'billing_required', tier: ent.tier, price: ent.price } };
|
|
18438
|
-
}
|
|
18439
|
-
if (typeof enabled === 'boolean') doc.enabled = enabled;
|
|
18440
|
-
if (doc.enabled && !doc.site_key) {
|
|
18441
|
-
const k = _bp_new_keys();
|
|
18442
|
-
doc.site_key = k.site_key;
|
|
18443
|
-
doc.secret_key = k.secret_key;
|
|
18444
|
-
}
|
|
18445
|
-
if (rotate_secret === true && doc.site_key) {
|
|
18446
|
-
if (doc.secret_key) await _bp_delete_index(_bp_index_id.secret(doc.secret_key));
|
|
18447
|
-
doc.secret_key = _bp_new_keys().secret_key;
|
|
18448
|
-
}
|
|
18449
|
-
if (escalation && typeof escalation === 'object') {
|
|
18450
|
-
doc.escalation = { ...(doc.escalation || _bp_default_escalation()), ..._bp_sanitize_escalation(escalation) };
|
|
18451
|
-
}
|
|
18452
|
-
if (custom_rules && typeof custom_rules === 'object') doc.custom_rules = bp_sanitize_rules(custom_rules);
|
|
18453
|
-
if (typeof require_on_contact_form === 'boolean') doc.require_on_contact_form = require_on_contact_form;
|
|
18454
|
-
_bp_flush_stats(doc);
|
|
18455
|
-
await _bp_save(doc);
|
|
18456
|
-
return await get_bot_protection_settings({ uid, profile_id });
|
|
18457
|
-
} catch (err) {
|
|
18458
|
-
return { code: -1, data: err.message || String(err) };
|
|
18459
|
-
}
|
|
18460
|
-
};
|
|
18461
|
-
|
|
18462
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
18463
|
-
// BOT PROTECTION - Surface 2 lifecycle (origin protection of a target).
|
|
18464
|
-
// These are called by app_module's thin cpi handlers over the broker (uid is
|
|
18465
|
-
// passed explicitly, already authenticated at the http layer), plus two
|
|
18466
|
-
// broker-only helpers the router uses at the edge. See docs/handoff_bot_protection_ishai.md.
|
|
18467
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
18468
|
-
|
|
18469
|
-
// Stable per-target slug so re-attaching the same target maps to one profile.
|
|
18470
|
-
const _bp_slug_for_target = (target) => 't_' + _bp_sha(String((target && target.id) || '')).slice(0, 16);
|
|
18471
|
-
const _bp_summary = (doc) => ({
|
|
18472
|
-
profile_id: doc._id,
|
|
18473
|
-
target: doc.target || null,
|
|
18474
|
-
enabled: doc.enabled === true,
|
|
18475
|
-
armed: doc.armed === true,
|
|
18476
|
-
plan_tier: doc.plan_tier || 'standard',
|
|
18477
|
-
surface: doc.surface || 'origin',
|
|
18478
|
-
site_key: doc.site_key || '',
|
|
18479
|
-
stats: _bp_merged_stats(doc),
|
|
18480
|
-
});
|
|
18481
|
-
|
|
18482
|
-
// IPv4 CIDR membership test (dependency-free) for allow/block-list rules.
|
|
18483
|
-
const _cidr_match = (cidr, ip) => {
|
|
18484
|
-
try {
|
|
18485
|
-
if (typeof cidr !== 'string' || cidr.indexOf('/') < 0) return false;
|
|
18486
|
-
const [range, bitsStr] = cidr.split('/');
|
|
18487
|
-
const bits = parseInt(bitsStr, 10);
|
|
18488
|
-
if (!/^\d+\.\d+\.\d+\.\d+$/.test(range) || !/^\d+\.\d+\.\d+\.\d+$/.test(ip) || !(bits >= 0 && bits <= 32)) return false;
|
|
18489
|
-
const toInt = (a) => a.split('.').reduce((s, o) => ((s << 8) + (parseInt(o, 10) & 255)) >>> 0, 0) >>> 0;
|
|
18490
|
-
const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0;
|
|
18491
|
-
return (toInt(range) & mask) === (toInt(ip) & mask);
|
|
18492
|
-
} catch (e) {
|
|
18493
|
-
return false;
|
|
18494
|
-
}
|
|
18495
|
-
};
|
|
18496
|
-
|
|
18497
|
-
export const bp_attach = async function (req) {
|
|
18498
|
-
try {
|
|
18499
|
-
const { uid, target, plan_tier, escalation } = req;
|
|
18500
|
-
if (!uid || !target || !target.id) return { code: -1, data: 'bad_request' };
|
|
18501
|
-
const tier = plan_tier === 'custom' ? 'custom' : 'standard';
|
|
18502
|
-
const ent = await _bp_ensure_entitled(uid, tier);
|
|
18503
|
-
if (!ent.ok) return { code: -402, data: { error: 'billing_required', tier: ent.tier, price: ent.price } };
|
|
18504
|
-
const slug = _bp_slug_for_target(target);
|
|
18505
|
-
const doc = await _bp_ensure_profile(uid, slug, { surface: 'origin', plan_tier: tier, target });
|
|
18506
|
-
doc.target = target;
|
|
18507
|
-
doc.plan_tier = tier;
|
|
18508
|
-
doc.surface = doc.surface === 'widget' ? 'both' : 'origin';
|
|
18509
|
-
doc.enabled = true;
|
|
18510
|
-
if (!doc.site_key) { const k = _bp_new_keys(); doc.site_key = k.site_key; doc.secret_key = k.secret_key; }
|
|
18511
|
-
if (escalation && typeof escalation === 'object') doc.escalation = { ...(doc.escalation || _bp_default_escalation()), ..._bp_sanitize_escalation(escalation) };
|
|
18512
|
-
_bp_flush_stats(doc);
|
|
18513
|
-
await _bp_save(doc);
|
|
18514
|
-
return { code: 1, data: _bp_summary(doc) };
|
|
18515
|
-
} catch (err) {
|
|
18516
|
-
return { code: -1, data: err.message || String(err) };
|
|
18517
|
-
}
|
|
18518
|
-
};
|
|
18519
|
-
|
|
18520
|
-
export const bp_get_profiles = async function (req) {
|
|
18521
|
-
try {
|
|
18522
|
-
const { uid } = req;
|
|
18523
|
-
if (!uid) return { code: -1, data: 'no uid' };
|
|
18524
|
-
const q = await db_module.find_couch_query(BP_DB, { selector: { docType: 'bot_protection_profile', owner_uid: uid }, limit: 500 }, true, true);
|
|
18525
|
-
const docs = q?.docs || [];
|
|
18526
|
-
return { code: 1, data: { profiles: docs.map(_bp_summary) } };
|
|
18527
|
-
} catch (err) {
|
|
18528
|
-
return { code: -1, data: err.message || String(err) };
|
|
18529
|
-
}
|
|
18530
|
-
};
|
|
18531
|
-
|
|
18532
|
-
export const bp_arm = async function (req) {
|
|
18533
|
-
try {
|
|
18534
|
-
const { uid, profile_id, app_id, armed, target } = req;
|
|
18535
|
-
if (!uid) return { code: -1, data: 'no uid' };
|
|
18536
|
-
let doc = null;
|
|
18537
|
-
if (profile_id) {
|
|
18538
|
-
doc = await _bp_load(profile_id);
|
|
18539
|
-
} else if (target && target.id) {
|
|
18540
|
-
doc = await _bp_ensure_profile(uid, _bp_slug_for_target(target), { surface: 'origin', target });
|
|
18541
|
-
if (!doc.target) doc.target = target;
|
|
18542
|
-
} else if (app_id) {
|
|
18543
|
-
const slug = _bp_slug_for_target({ id: app_id });
|
|
18544
|
-
doc = await _bp_load(_bp_profile_id(uid, slug));
|
|
18545
|
-
if (!doc) doc = await _bp_ensure_profile(uid, slug, { surface: 'origin', target: { type: 'internal', kind: 'app', id: app_id, label: app_id, region: null } });
|
|
18546
|
-
}
|
|
18547
|
-
if (!doc) return { code: -404, data: 'profile_not_found' };
|
|
18548
|
-
if (doc.owner_uid !== uid) return { code: -403, data: 'not_owner' };
|
|
18549
|
-
const want = armed === true;
|
|
18550
|
-
if (want) {
|
|
18551
|
-
const ent = await _bp_ensure_entitled(uid, doc.plan_tier || 'standard');
|
|
18552
|
-
if (!ent.ok) return { code: -402, data: { error: 'billing_required', tier: ent.tier, price: ent.price } };
|
|
18553
|
-
if (!doc.site_key) { const k = _bp_new_keys(); doc.site_key = k.site_key; doc.secret_key = k.secret_key; }
|
|
18554
|
-
doc.enabled = true;
|
|
18555
|
-
}
|
|
18556
|
-
doc.armed = want;
|
|
18557
|
-
_bp_flush_stats(doc);
|
|
18558
|
-
await _bp_save(doc);
|
|
18559
|
-
return { code: 1, data: _bp_summary(doc) };
|
|
18560
|
-
} catch (err) {
|
|
18561
|
-
return { code: -1, data: err.message || String(err) };
|
|
18562
|
-
}
|
|
18563
|
-
};
|
|
18564
|
-
|
|
18565
|
-
export const bp_detach = async function (req) {
|
|
18566
|
-
try {
|
|
18567
|
-
const { uid, profile_id } = req;
|
|
18568
|
-
const doc = await _bp_load(profile_id);
|
|
18569
|
-
if (!doc) return { code: 1, data: { ok: true } };
|
|
18570
|
-
if (doc.owner_uid !== uid) return { code: -403, data: 'not_owner' };
|
|
18571
|
-
if (doc.site_key) await _bp_delete_index(_bp_index_id.key(doc.site_key));
|
|
18572
|
-
if (doc.secret_key) await _bp_delete_index(_bp_index_id.secret(doc.secret_key));
|
|
18573
|
-
const tkey = _bp_target_key(doc);
|
|
18574
|
-
if (tkey) await _bp_delete_index(_bp_index_id.target(tkey));
|
|
18575
|
-
try {
|
|
18576
|
-
const r = await db_module.get_couch_doc(BP_DB, doc._id, true);
|
|
18577
|
-
if (r?.data?._rev) await db_module.delete_couch_doc(BP_DB, doc._id, r.data._rev);
|
|
18578
|
-
} catch (e) {}
|
|
18579
|
-
_bp_stat_buf.delete(doc._id);
|
|
18580
|
-
return { code: 1, data: { ok: true } };
|
|
18581
|
-
} catch (err) {
|
|
18582
|
-
return { code: -1, data: err.message || String(err) };
|
|
18583
|
-
}
|
|
18584
|
-
};
|
|
18585
|
-
|
|
18586
|
-
export const bp_set_custom_rules = async function (req) {
|
|
18587
|
-
try {
|
|
18588
|
-
const { uid, profile_id, rules } = req;
|
|
18589
|
-
const doc = await _bp_load(profile_id);
|
|
18590
|
-
if (!doc) return { code: -404, data: 'profile_not_found' };
|
|
18591
|
-
if (doc.owner_uid !== uid) return { code: -403, data: 'not_owner' };
|
|
18592
|
-
const ent = await _bp_ensure_entitled(uid, 'custom');
|
|
18593
|
-
if (!ent.ok) return { code: -402, data: { error: 'billing_required', tier: 'custom', price: 5 } };
|
|
18594
|
-
doc.plan_tier = 'custom';
|
|
18595
|
-
doc.custom_rules = bp_sanitize_rules(rules || {});
|
|
18596
|
-
_bp_flush_stats(doc);
|
|
18597
|
-
await _bp_save(doc);
|
|
18598
|
-
return { code: 1, data: _bp_summary(doc) };
|
|
18599
|
-
} catch (err) {
|
|
18600
|
-
return { code: -1, data: err.message || String(err) };
|
|
18601
|
-
}
|
|
18602
|
-
};
|
|
18603
|
-
|
|
18604
|
-
// Broker-only: the router resolves the guarding profile for a target at the edge.
|
|
18605
|
-
export const bp_resolve_target = async function (req) {
|
|
18606
|
-
try {
|
|
18607
|
-
const { target_key } = req;
|
|
18608
|
-
if (!target_key) return { code: -404, data: 'no_target' };
|
|
18609
|
-
const idx = await _bp_load_index(_bp_index_id.target(target_key));
|
|
18610
|
-
if (!idx) return { code: -404, data: 'not_found' };
|
|
18611
|
-
const doc = await _bp_load(idx.profile_id);
|
|
18612
|
-
if (!doc) return { code: -404, data: 'not_found' };
|
|
18613
|
-
return {
|
|
18614
|
-
code: 1,
|
|
18615
|
-
data: {
|
|
18616
|
-
profile_id: doc._id,
|
|
18617
|
-
armed: doc.armed === true,
|
|
18618
|
-
enabled: doc.enabled === true,
|
|
18619
|
-
plan_tier: doc.plan_tier || 'standard',
|
|
18620
|
-
site_key: doc.site_key || '',
|
|
18621
|
-
custom_rules: doc.custom_rules || {},
|
|
18622
|
-
escalation: doc.escalation || _bp_default_escalation(),
|
|
18623
|
-
target: doc.target || null,
|
|
18624
|
-
},
|
|
18625
|
-
};
|
|
18626
|
-
} catch (err) {
|
|
18627
|
-
return { code: -404, data: err.message || String(err) };
|
|
18628
|
-
}
|
|
18629
|
-
};
|
|
18630
|
-
|
|
18631
|
-
// Broker-only: evaluate the custom rules for an inbound request context and
|
|
18632
|
-
// return the enforcement action. Standard tier ignores custom_rules (challenge
|
|
18633
|
-
// when armed); custom tier honors the full rule set.
|
|
18634
|
-
export const evaluate_custom_rules = async function (req) {
|
|
18635
|
-
try {
|
|
18636
|
-
const { profile, ctx } = req;
|
|
18637
|
-
const d = _bp_defaults();
|
|
18638
|
-
const esc = (profile && profile.escalation) || _bp_default_escalation();
|
|
18639
|
-
const out = { action: 'challenge', start_level: 0, aggressiveness: esc.aggressiveness || d.aggressiveness, clearance_ttl_ms: esc.clearance_ttl_ms || d.clearance_ttl_ms };
|
|
18640
|
-
const c = ctx || {};
|
|
18641
|
-
const rules = profile && profile.plan_tier === 'custom' && profile.custom_rules ? profile.custom_rules : null;
|
|
18642
|
-
const inList = (list, ip, country) => (list || []).some((e) => e === ip || e === country || _cidr_match(e, ip));
|
|
18643
|
-
if (rules) {
|
|
18644
|
-
if (inList(rules.block_list, c.ip, c.country)) return { code: 1, data: { ...out, action: 'block' } };
|
|
18645
|
-
if (inList(rules.allow_list, c.ip, c.country)) return { code: 1, data: { ...out, action: 'allow' } };
|
|
18646
|
-
if (rules.reputation && rules.reputation.allow_verified_search_bots && c.verified_bot) return { code: 1, data: { ...out, action: 'allow' } };
|
|
18647
|
-
if (rules.geo && rules.geo.mode && rules.geo.mode !== 'off' && c.country && (rules.geo.countries || []).includes(c.country)) {
|
|
18648
|
-
if (rules.geo.mode === 'block') return { code: 1, data: { ...out, action: 'block' } };
|
|
18649
|
-
if (rules.geo.mode === 'allow') return { code: 1, data: { ...out, action: 'allow' } };
|
|
18650
|
-
out.action = 'challenge';
|
|
18651
|
-
}
|
|
18652
|
-
if (Array.isArray(rules.paths) && c.path) {
|
|
18653
|
-
for (const p of rules.paths) {
|
|
18654
|
-
if (p && p.pattern && c.path.indexOf(p.pattern) === 0) {
|
|
18655
|
-
if (p.action === 'block') return { code: 1, data: { ...out, action: 'block' } };
|
|
18656
|
-
if (p.action === 'allow') return { code: 1, data: { ...out, action: 'allow' } };
|
|
18657
|
-
out.action = 'challenge';
|
|
18658
|
-
break;
|
|
18659
|
-
}
|
|
18660
|
-
}
|
|
18661
|
-
}
|
|
18662
|
-
if (rules.hours && rules.hours.mode === 'challenge' && Array.isArray(rules.hours.list) && typeof c.hour === 'number' && rules.hours.list.includes(c.hour)) out.action = 'challenge';
|
|
18663
|
-
if (rules.days && rules.days.mode === 'challenge' && Array.isArray(rules.days.list) && typeof c.day === 'number' && rules.days.list.includes(c.day)) out.action = 'challenge';
|
|
18664
|
-
if (rules.require_referer && !c.has_referer) out.action = 'challenge';
|
|
18665
|
-
if (['low', 'medium', 'high'].includes(rules.aggressiveness)) out.aggressiveness = rules.aggressiveness;
|
|
18666
|
-
if (Number.isFinite(Number(rules.clearance_ttl_ms))) out.clearance_ttl_ms = Number(rules.clearance_ttl_ms);
|
|
18667
|
-
}
|
|
18668
|
-
return { code: 1, data: out };
|
|
18669
|
-
} catch (err) {
|
|
18670
|
-
return { code: 1, data: { action: 'challenge', start_level: 0 } };
|
|
18671
|
-
}
|
|
18672
|
-
};
|
|
18673
|
-
|
|
18674
|
-
// Broker-only: the origin interstitial passes a captcha response token (from the
|
|
18675
|
-
// embedded widget) here. Resolves the profile by site_key, verifies and burns the
|
|
18676
|
-
// token, and returns the guarded target so the router can mint a clearance cookie.
|
|
18677
|
-
export const bp_consume_response = async function (req) {
|
|
18678
|
-
try {
|
|
18679
|
-
const { site_key, response } = req;
|
|
18680
|
-
const profile = await _bp_resolve_by_site_key(site_key);
|
|
18681
|
-
if (!profile || profile.enabled !== true) return { code: 1, data: { ok: false, reason: 'unknown_site' } };
|
|
18682
|
-
const v = verify_response_token(profile.site_key, response);
|
|
18683
|
-
if (!v.ok) return { code: 1, data: { ok: false, reason: v.reason } };
|
|
18684
|
-
const esc = profile.escalation || _bp_default_escalation();
|
|
18685
|
-
const d = _bp_defaults();
|
|
18686
|
-
return { code: 1, data: { ok: true, level: v.level, target: _bp_target_key(profile), clearance_ttl_ms: esc.clearance_ttl_ms || d.clearance_ttl_ms } };
|
|
18687
|
-
} catch (err) {
|
|
18688
|
-
return { code: 1, data: { ok: false, reason: 'error' } };
|
|
18689
|
-
}
|
|
18690
|
-
};
|
|
18691
|
-
|
|
18692
|
-
// Broker-only: the router caches the set of armed targets (refreshed periodically)
|
|
18693
|
-
// so the enforcement hot path is a local lookup and unguarded traffic never hits
|
|
18694
|
-
// the broker.
|
|
18695
|
-
export const bp_armed_targets = async function () {
|
|
18696
|
-
try {
|
|
18697
|
-
const q = await db_module.find_couch_query(BP_DB, { selector: { docType: 'bot_protection_profile', armed: true }, fields: ['_id', 'target', 'site_key'], limit: 2000 }, true, true);
|
|
18698
|
-
const rows = (q?.docs || []).map((doc) => ({ target_key: _bp_target_key(doc), site_key: doc.site_key })).filter((x) => x.target_key && x.site_key);
|
|
18699
|
-
return { code: 1, data: rows };
|
|
18700
|
-
} catch (err) {
|
|
18701
|
-
return { code: -1, data: err.message || String(err) };
|
|
18702
|
-
}
|
|
18703
|
-
};
|
package/index_ms.mjs
CHANGED
|
@@ -129,6 +129,10 @@ export const submit_chat_gpt_prompt = async function (...args) {
|
|
|
129
129
|
return await broker.send_to_queue("submit_chat_gpt_prompt", ...args);
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
+
export const submit_structured_prompt = async function (...args) {
|
|
133
|
+
return await broker.send_to_queue("submit_structured_prompt", ...args);
|
|
134
|
+
};
|
|
135
|
+
|
|
132
136
|
export const triage_error_incident = async function (...args) {
|
|
133
137
|
return await broker.send_to_queue("triage_error_incident", ...args);
|
|
134
138
|
};
|
|
@@ -360,71 +364,3 @@ export const get_contact_form_bootstrap = async function (...args) {
|
|
|
360
364
|
export const contact_form_submit = async function (...args) {
|
|
361
365
|
return await broker.send_to_queue("contact_form_submit", ...args);
|
|
362
366
|
};
|
|
363
|
-
|
|
364
|
-
export const mint_clearance_token = async function (...args) {
|
|
365
|
-
return await broker.send_to_queue("mint_clearance_token", ...args);
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
export const verify_clearance_token = async function (...args) {
|
|
369
|
-
return await broker.send_to_queue("verify_clearance_token", ...args);
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
export const get_captcha_bootstrap = async function (...args) {
|
|
373
|
-
return await broker.send_to_queue("get_captcha_bootstrap", ...args);
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
export const captcha_verify = async function (...args) {
|
|
377
|
-
return await broker.send_to_queue("captcha_verify", ...args);
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
export const captcha_siteverify = async function (...args) {
|
|
381
|
-
return await broker.send_to_queue("captcha_siteverify", ...args);
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
export const bp_sanitize_rules = async function (...args) {
|
|
385
|
-
return await broker.send_to_queue("bp_sanitize_rules", ...args);
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
export const get_bot_protection_settings = async function (...args) {
|
|
389
|
-
return await broker.send_to_queue("get_bot_protection_settings", ...args);
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
export const update_bot_protection_settings = async function (...args) {
|
|
393
|
-
return await broker.send_to_queue("update_bot_protection_settings", ...args);
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
export const bp_attach = async function (...args) {
|
|
397
|
-
return await broker.send_to_queue("bp_attach", ...args);
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
export const bp_get_profiles = async function (...args) {
|
|
401
|
-
return await broker.send_to_queue("bp_get_profiles", ...args);
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
export const bp_arm = async function (...args) {
|
|
405
|
-
return await broker.send_to_queue("bp_arm", ...args);
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
export const bp_detach = async function (...args) {
|
|
409
|
-
return await broker.send_to_queue("bp_detach", ...args);
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
export const bp_set_custom_rules = async function (...args) {
|
|
413
|
-
return await broker.send_to_queue("bp_set_custom_rules", ...args);
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
export const bp_resolve_target = async function (...args) {
|
|
417
|
-
return await broker.send_to_queue("bp_resolve_target", ...args);
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
export const evaluate_custom_rules = async function (...args) {
|
|
421
|
-
return await broker.send_to_queue("evaluate_custom_rules", ...args);
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
export const bp_consume_response = async function (...args) {
|
|
425
|
-
return await broker.send_to_queue("bp_consume_response", ...args);
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
export const bp_armed_targets = async function (...args) {
|
|
429
|
-
return await broker.send_to_queue("bp_armed_targets", ...args);
|
|
430
|
-
};
|
package/index_msa.mjs
CHANGED
|
@@ -129,6 +129,10 @@ export const submit_chat_gpt_prompt = function (...args) {
|
|
|
129
129
|
broker.send_to_queue_async("submit_chat_gpt_prompt", ...args);
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
+
export const submit_structured_prompt = function (...args) {
|
|
133
|
+
broker.send_to_queue_async("submit_structured_prompt", ...args);
|
|
134
|
+
};
|
|
135
|
+
|
|
132
136
|
export const triage_error_incident = function (...args) {
|
|
133
137
|
broker.send_to_queue_async("triage_error_incident", ...args);
|
|
134
138
|
};
|
|
@@ -360,71 +364,3 @@ export const get_contact_form_bootstrap = function (...args) {
|
|
|
360
364
|
export const contact_form_submit = function (...args) {
|
|
361
365
|
broker.send_to_queue_async("contact_form_submit", ...args);
|
|
362
366
|
};
|
|
363
|
-
|
|
364
|
-
export const mint_clearance_token = function (...args) {
|
|
365
|
-
broker.send_to_queue_async("mint_clearance_token", ...args);
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
export const verify_clearance_token = function (...args) {
|
|
369
|
-
broker.send_to_queue_async("verify_clearance_token", ...args);
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
export const get_captcha_bootstrap = function (...args) {
|
|
373
|
-
broker.send_to_queue_async("get_captcha_bootstrap", ...args);
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
export const captcha_verify = function (...args) {
|
|
377
|
-
broker.send_to_queue_async("captcha_verify", ...args);
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
export const captcha_siteverify = function (...args) {
|
|
381
|
-
broker.send_to_queue_async("captcha_siteverify", ...args);
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
export const bp_sanitize_rules = function (...args) {
|
|
385
|
-
broker.send_to_queue_async("bp_sanitize_rules", ...args);
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
export const get_bot_protection_settings = function (...args) {
|
|
389
|
-
broker.send_to_queue_async("get_bot_protection_settings", ...args);
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
export const update_bot_protection_settings = function (...args) {
|
|
393
|
-
broker.send_to_queue_async("update_bot_protection_settings", ...args);
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
export const bp_attach = function (...args) {
|
|
397
|
-
broker.send_to_queue_async("bp_attach", ...args);
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
export const bp_get_profiles = function (...args) {
|
|
401
|
-
broker.send_to_queue_async("bp_get_profiles", ...args);
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
export const bp_arm = function (...args) {
|
|
405
|
-
broker.send_to_queue_async("bp_arm", ...args);
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
export const bp_detach = function (...args) {
|
|
409
|
-
broker.send_to_queue_async("bp_detach", ...args);
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
export const bp_set_custom_rules = function (...args) {
|
|
413
|
-
broker.send_to_queue_async("bp_set_custom_rules", ...args);
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
export const bp_resolve_target = function (...args) {
|
|
417
|
-
broker.send_to_queue_async("bp_resolve_target", ...args);
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
export const evaluate_custom_rules = function (...args) {
|
|
421
|
-
broker.send_to_queue_async("evaluate_custom_rules", ...args);
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
export const bp_consume_response = function (...args) {
|
|
425
|
-
broker.send_to_queue_async("bp_consume_response", ...args);
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
export const bp_armed_targets = function (...args) {
|
|
429
|
-
broker.send_to_queue_async("bp_armed_targets", ...args);
|
|
430
|
-
};
|