@xuda.io/ai_module 1.1.5646 → 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 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,
@@ -11285,6 +11318,101 @@ async function normalizeBase64To1024(
11285
11318
  return normalizedBuffer.toString('base64');
11286
11319
  }
11287
11320
 
11321
+ // Upload-time content check for a profile picture or a business logo.
11322
+ //
11323
+ // Deliberately NOT identity verification. It answers "is this the right KIND of
11324
+ // image, and is it good enough to use", never "is this the right person". There
11325
+ // is no face embedding, no matching against a reference, and nothing about the
11326
+ // image is retained — so this does not cross the biometric line drawn in
11327
+ // docs/plans/xuda-verify-consent.md, which stays gated on the face-model
11328
+ // decision and that document's pre-capture prerequisites. Identity matching is
11329
+ // still the unbuilt half of phase 2, see
11330
+ // docs/plans/handoff_profile_image_verification.md.
11331
+ //
11332
+ // The same classification already existed as a private closure inside
11333
+ // get_profile_avatar (inspect_person_in_image), but it only ran at AVATAR
11334
+ // GENERATION time, which is after verification. Users need the answer while
11335
+ // they are still looking at the picker, so it lives here as its own method and
11336
+ // covers the business/logo case the original never did.
11337
+ export const inspect_profile_picture = async function (req, job_id, headers) {
11338
+ try {
11339
+ const data = req?.data || req || {};
11340
+ const uid = req?.uid;
11341
+ const url = data.url;
11342
+ const account_type = data.account_type === 'business' ? 'business' : 'personal';
11343
+
11344
+ if (!uid) return { code: -401, data: 'not authorized' };
11345
+ if (!url) return { code: -1, data: 'url is required' };
11346
+
11347
+ // Attribution only; a missing profile must not block the check.
11348
+ let account_profile_info;
11349
+ try {
11350
+ account_profile_info = await account_ms.get_active_account_profile_info(uid);
11351
+ } catch (e) {}
11352
+
11353
+ const image_blob_ret = await get_image_blob_from_downloaded_image(url);
11354
+ if (!image_blob_ret?.image_blob) return { code: -1, data: 'could not read that image' };
11355
+ const base64 = Buffer.from(await image_blob_ret.image_blob.arrayBuffer()).toString('base64');
11356
+
11357
+ const is_business = account_type === 'business';
11358
+ const instruction = is_business
11359
+ ? `This image was uploaded as the COMPANY LOGO for a business account. Decide whether it is usable as that logo or brand mark. Reject a photograph of a person, a selfie, a stock photo, a screenshot, a meme, an unrelated object, or an image with no identifiable mark. Also reject it if it is too blurry, too small or too cropped to use.`
11360
+ : `This image was uploaded as a PERSONAL PROFILE PICTURE. Decide whether it is a genuine photograph of a real person, suitable as an authentic account portrait. Reject a company logo, an illustration, a cartoon, an obviously AI-generated render, an object, an animal, a screenshot, or a group shot with no single clear subject. Also reject it if the face is not visible, heavily cropped, very blurry, or too small.`;
11361
+
11362
+ const ret = await submit_chat_gpt_prompt({
11363
+ uid,
11364
+ model: _conf.default_ai_model,
11365
+ prompt: [
11366
+ {
11367
+ role: 'user',
11368
+ content: [
11369
+ { type: 'input_text', text: `${instruction}\n\nBe practical rather than strict: an ordinary phone photo in ordinary lighting is fine. Only reject when a real person would agree it is the wrong kind of image or genuinely unusable.` },
11370
+ { type: 'input_image', image_url: `data:image/png;base64,${base64}` },
11371
+ ],
11372
+ },
11373
+ ],
11374
+ response_format: z.object({
11375
+ acceptable: z.boolean().describe('true only if the image is the right kind AND usable quality'),
11376
+ reason_code: z
11377
+ .string()
11378
+ .describe(
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',
11380
+ ),
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.'),
11391
+ }),
11392
+ metadata: { func: 'inspect_profile_picture', account_type },
11393
+ account_profile_info,
11394
+ });
11395
+
11396
+ if (ret.code < 0) return ret;
11397
+
11398
+ const res = JSON5.parse(ret.data);
11399
+ return {
11400
+ code: 1,
11401
+ data: {
11402
+ acceptable: Boolean(res?.acceptable),
11403
+ reason_code: res?.reason_code || (res?.acceptable ? 'ok' : 'unusable'),
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 || '',
11407
+ account_type,
11408
+ },
11409
+ };
11410
+ } catch (err) {
11411
+ console.error('[inspect_profile_picture]', err.message);
11412
+ return { code: -1, data: err.message };
11413
+ }
11414
+ };
11415
+
11288
11416
  const get_image_blob_from_downloaded_image = async function (url) {
11289
11417
  async function hasTransparentBackground(filePath) {
11290
11418
  const image = sharp(filePath);
@@ -17346,11 +17474,12 @@ export const get_contact_form_bootstrap = async function (req) {
17346
17474
 
17347
17475
  const canonical_id = `${account_profile_info.uid}.${account_profile_info.account_profile_id}`;
17348
17476
  // Bot protection: surface the captcha site key when the owner requires the
17349
- // "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.
17350
17479
  let captcha_site_key = '';
17351
17480
  try {
17352
- const bp = await _bp_load(_bp_profile_id(account_profile_info.uid, account_profile_info.account_profile_id));
17353
- if (bp && bp.enabled === true && bp.require_on_contact_form === true && bp.site_key) captcha_site_key = bp.site_key;
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 || '';
17354
17483
  } catch (e) {}
17355
17484
  return {
17356
17485
  code: 1,
@@ -17493,12 +17622,15 @@ export const contact_form_submit = async function (req, job_id, headers) {
17493
17622
 
17494
17623
  // Bot protection: when the owner requires the "I'm not a robot" check on their
17495
17624
  // contact form, verify the captcha response (single-use) before accepting.
17496
- let _bp_prof = null;
17497
- try { _bp_prof = await _bp_load(_bp_profile_id(account_profile_info.uid, account_profile_info.account_profile_id)); } catch (e) {}
17498
- if (_bp_prof && _bp_prof.enabled === true && _bp_prof.require_on_contact_form === true) {
17499
- const cr = await bp_consume_response({ site_key: _bp_prof.site_key, response: req.captcha_response });
17500
- if (!cr || !cr.data || cr.data.ok !== true) return { code: -401, data: 'captcha_failed' };
17501
- }
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) {}
17502
17634
 
17503
17635
  const owner_uid = account_profile_info.uid;
17504
17636
  const config = _sanitize_contact_form_config(ap_doc.contact_form_config);
@@ -17639,6 +17771,10 @@ const contact_ticket_conversation = async function (req, job_id, headers) {
17639
17771
  let html = null;
17640
17772
  try {
17641
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,
17642
17778
  style: profile_doc.email_template?.style,
17643
17779
  body_text: body,
17644
17780
  profile_name: profile_doc.profile_name,
@@ -17710,910 +17846,3 @@ const contact_ticket_conversation = async function (req, job_id, headers) {
17710
17846
  return { code: -15, data: err.message };
17711
17847
  }
17712
17848
  };
17713
-
17714
- ///////////////////////////////////////////////////////////////////////////////
17715
- // BOT PROTECTION ("I'm not a robot") - verification engine
17716
- //
17717
- // Escalation ladder: L0 checkbox + passive signals + proof of work -> L1 image
17718
- // challenge (from-scratch SVG synthetic tiles) -> L2 AI check (score, then a
17719
- // one-shot AI-generated challenge). Two surfaces consume it: the embeddable
17720
- // widget (public site_key + server-side secret_key siteverify, reCAPTCHA shaped)
17721
- // and the origin interstitial (router). This generalizes the contact-form
17722
- // human-detection token above. Docs live in xuda_master so the router edge can
17723
- // resolve them in every region. See docs/handoff_bot_protection_ishai.md.
17724
- ///////////////////////////////////////////////////////////////////////////////
17725
-
17726
- const BP_DB = 'xuda_master';
17727
- const _bp_conf = () => (_conf && _conf.bot_protection) || {};
17728
- const _bp_defaults = () => ({
17729
- pow_difficulty: 16,
17730
- session_token_ttl_ms: 900000,
17731
- response_token_ttl_ms: 120000,
17732
- clearance_ttl_ms: 1800000,
17733
- min_age_ms: 800,
17734
- image_grid: 9,
17735
- image_correct_min: 2,
17736
- aggressiveness: 'medium',
17737
- l1_score_low: 0.35,
17738
- l2_score_low: 0.7,
17739
- ...(_bp_conf().defaults || {}),
17740
- });
17741
-
17742
- // Fleet-shared HMAC key, same derivation style as the contact-form key so the
17743
- // router and this module sign and verify the same tokens without distributing a
17744
- // new secret.
17745
- const _bot_hmac_key = function () {
17746
- const seed = String(_conf?.gmail?.clientSecret || _conf?.domain || 'xuda');
17747
- return crypto.createHash('sha256').update(`xuda_bot_protection_v1:${seed}`).digest();
17748
- };
17749
- const _bp_sig = (input) => crypto.createHmac('sha256', _bot_hmac_key()).update(String(input)).digest('hex').slice(0, 32);
17750
- const _bp_rand = (n) => crypto.randomBytes(n).toString('base64url');
17751
- const _bp_sha = (s) => crypto.createHash('sha256').update(String(s)).digest('hex');
17752
- const _bp_aggr = (name) => ({ low: 0, medium: 1, high: 2 }[name] ?? 1);
17753
- const _bp_pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
17754
-
17755
- // --- Clearance / session tokens ------------------------------------------
17756
- // Format: xcl1.<scope>.<ts>.<ttl>.<sig>. The subject is bound through the
17757
- // signature, not stored in the token. scope in { session, response, clearance }.
17758
- export const mint_clearance_token = function (scope, subject, ttl_ms, ts = Date.now()) {
17759
- const sig = _bp_sig(`${scope}.${subject}.${ts}.${ttl_ms}`);
17760
- return `xcl1.${scope}.${ts}.${ttl_ms}.${sig}`;
17761
- };
17762
- export const verify_clearance_token = function (scope, subject, token, min_age_ms = 0) {
17763
- const p = String(token || '').split('.');
17764
- if (p.length !== 5 || p[0] !== 'xcl1' || p[1] !== scope) return { ok: false, reason: 'bad_token' };
17765
- const ts = Number(p[2]);
17766
- const ttl = Number(p[3]);
17767
- if (!Number.isFinite(ts) || !Number.isFinite(ttl)) return { ok: false, reason: 'bad_token' };
17768
- const expected = _bp_sig(`${scope}.${subject}.${ts}.${ttl}`);
17769
- let ok = false;
17770
- try { ok = crypto.timingSafeEqual(Buffer.from(p[4]), Buffer.from(expected)); } catch (e) { ok = false; }
17771
- if (!ok) return { ok: false, reason: 'bad_sig' };
17772
- const age = Date.now() - ts;
17773
- if (age > ttl) return { ok: false, reason: 'expired' };
17774
- if (age < min_age_ms) return { ok: false, reason: 'too_fast' };
17775
- return { ok: true, ts };
17776
- };
17777
-
17778
- // --- Response tokens (returned on PASS, verified by siteverify) -----------
17779
- // Self-contained (survives a cross-process hop) and single-use (best-effort
17780
- // per-process burn set, same posture as the contact-form throttles).
17781
- const _bp_used_responses = new Map(); // jti -> expiry ts
17782
- const _bp_burn = (jti, ttl) => {
17783
- _bp_used_responses.set(jti, Date.now() + ttl);
17784
- if (_bp_used_responses.size > 20000) {
17785
- const now = Date.now();
17786
- for (const [k, exp] of _bp_used_responses) if (exp < now) _bp_used_responses.delete(k);
17787
- }
17788
- };
17789
- const _bp_is_burned = (jti) => {
17790
- const exp = _bp_used_responses.get(jti);
17791
- if (!exp) return false;
17792
- if (exp < Date.now()) { _bp_used_responses.delete(jti); return false; }
17793
- return true;
17794
- };
17795
- const mint_response_token = function (site_key, level, score, ttl_ms) {
17796
- const ts = Date.now();
17797
- const jti = _bp_rand(9);
17798
- const score100 = Math.max(0, Math.min(100, Math.round((score || 0) * 100)));
17799
- const sig = _bp_sig(`resp.${site_key}.${ts}.${ttl_ms}.${level}.${score100}.${jti}`);
17800
- return `xrsp1.${ts}.${ttl_ms}.${level}.${score100}.${jti}.${sig}`;
17801
- };
17802
- const verify_response_token = function (site_key, token) {
17803
- const p = String(token || '').split('.');
17804
- if (p.length !== 7 || p[0] !== 'xrsp1') return { ok: false, reason: 'bad_token' };
17805
- const [, ts_s, ttl_s, level_s, score_s, jti, sig] = p;
17806
- const ts = Number(ts_s);
17807
- const ttl = Number(ttl_s);
17808
- if (!Number.isFinite(ts) || !Number.isFinite(ttl)) return { ok: false, reason: 'bad_token' };
17809
- const expected = _bp_sig(`resp.${site_key}.${ts}.${ttl}.${level_s}.${score_s}.${jti}`);
17810
- let ok = false;
17811
- try { ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); } catch (e) { ok = false; }
17812
- if (!ok) return { ok: false, reason: 'bad_sig' };
17813
- if (Date.now() - ts > ttl) return { ok: false, reason: 'expired' };
17814
- if (_bp_is_burned(jti)) return { ok: false, reason: 'replayed' };
17815
- _bp_burn(jti, ttl);
17816
- return { ok: true, level: Number(level_s), score: Number(score_s) / 100 };
17817
- };
17818
-
17819
- // --- Data model (docs in xuda_master) ------------------------------------
17820
- const _bp_profile_id = (uid, slug) => `bot_protection:${uid}.${slug}`;
17821
- const _bp_target_key = (doc) => (doc && doc.target && doc.target.id ? String(doc.target.id) : '');
17822
- const _bp_index_id = {
17823
- key: (k) => `bpk::${k}`,
17824
- secret: (s) => `bps::${_bp_sha(s)}`,
17825
- target: (t) => `bpt::${t}`,
17826
- };
17827
- const _bp_load = async (profile_id) => {
17828
- const r = await db_module.get_couch_doc(BP_DB, profile_id, true);
17829
- return r && r.code > 0 && r.data && r.data._id ? r.data : null;
17830
- };
17831
- const _bp_load_index = async (id) => {
17832
- const r = await db_module.get_couch_doc(BP_DB, id, true);
17833
- return r && r.code > 0 && r.data && r.data.profile_id ? r.data : null;
17834
- };
17835
- const _bp_write_index = async (id, profile_id) => {
17836
- const existing = await _bp_load_index(id);
17837
- const doc = existing
17838
- ? { ...existing, profile_id, ts: Date.now() }
17839
- : { _id: id, docType: 'bot_protection_index', profile_id, ts: Date.now() };
17840
- await db_module.save_couch_doc(BP_DB, doc);
17841
- };
17842
- const _bp_delete_index = async (id) => {
17843
- const r = await db_module.get_couch_doc(BP_DB, id, true);
17844
- if (r && r.code > 0 && r.data && r.data._rev) {
17845
- try { await db_module.delete_couch_doc(BP_DB, id, r.data._rev); } catch (e) {}
17846
- }
17847
- };
17848
- const _bp_resolve_by_site_key = async (site_key) => {
17849
- const idx = await _bp_load_index(_bp_index_id.key(site_key));
17850
- return idx ? await _bp_load(idx.profile_id) : null;
17851
- };
17852
- const _bp_resolve_by_secret = async (secret) => {
17853
- const idx = await _bp_load_index(_bp_index_id.secret(secret));
17854
- return idx ? await _bp_load(idx.profile_id) : null;
17855
- };
17856
- const _bp_new_keys = () => ({ site_key: _bp_rand(18), secret_key: _bp_rand(32) });
17857
-
17858
- const _bp_default_escalation = () => {
17859
- const d = _bp_defaults();
17860
- return {
17861
- pow_difficulty: d.pow_difficulty,
17862
- aggressiveness: d.aggressiveness,
17863
- clearance_ttl_ms: d.clearance_ttl_ms,
17864
- response_token_ttl_ms: d.response_token_ttl_ms,
17865
- session_token_ttl_ms: d.session_token_ttl_ms,
17866
- l1_enabled: true,
17867
- l2_enabled: true,
17868
- };
17869
- };
17870
- const _bp_ensure_profile = async (uid, slug, seed = {}) => {
17871
- const id = _bp_profile_id(uid, slug);
17872
- let doc = await _bp_load(id);
17873
- if (!doc) {
17874
- const d = Date.now();
17875
- doc = {
17876
- _id: id,
17877
- docType: 'bot_protection_profile',
17878
- owner_uid: uid,
17879
- enabled: false,
17880
- surface: seed.surface || 'widget',
17881
- plan_tier: seed.plan_tier || 'standard',
17882
- site_key: '',
17883
- secret_key: '',
17884
- target: seed.target || null,
17885
- escalation: { start_level: 0, ..._bp_default_escalation() },
17886
- custom_rules: {},
17887
- require_on_contact_form: false,
17888
- armed: false,
17889
- stats: { served: 0, passed: 0, challenged: 0, blocked: 0 },
17890
- date_created_ts: d,
17891
- ts: d,
17892
- };
17893
- }
17894
- return doc;
17895
- };
17896
- const _bp_save = async (doc) => {
17897
- doc.ts = Date.now();
17898
- const ret = await db_module.save_couch_doc(BP_DB, doc);
17899
- if (doc.site_key) await _bp_write_index(_bp_index_id.key(doc.site_key), doc._id);
17900
- if (doc.secret_key) await _bp_write_index(_bp_index_id.secret(doc.secret_key), doc._id);
17901
- const tkey = _bp_target_key(doc);
17902
- if (tkey) await _bp_write_index(_bp_index_id.target(tkey), doc._id);
17903
- return ret;
17904
- };
17905
-
17906
- // --- Stats (in-process buffer, persisted opportunistically) ---------------
17907
- const _bp_stat_buf = new Map();
17908
- const _bp_bump_stat = (profile_id, field, n = 1) => {
17909
- const b = _bp_stat_buf.get(profile_id) || { served: 0, passed: 0, challenged: 0, blocked: 0 };
17910
- b[field] = (b[field] || 0) + n;
17911
- _bp_stat_buf.set(profile_id, b);
17912
- };
17913
- const _bp_merged_stats = (doc) => {
17914
- const base = doc.stats || { served: 0, passed: 0, challenged: 0, blocked: 0 };
17915
- const b = _bp_stat_buf.get(doc._id) || {};
17916
- return {
17917
- served: (base.served || 0) + (b.served || 0),
17918
- passed: (base.passed || 0) + (b.passed || 0),
17919
- challenged: (base.challenged || 0) + (b.challenged || 0),
17920
- blocked: (base.blocked || 0) + (b.blocked || 0),
17921
- };
17922
- };
17923
- const _bp_flush_stats = (doc) => {
17924
- if (_bp_stat_buf.has(doc._id)) {
17925
- doc.stats = _bp_merged_stats(doc);
17926
- _bp_stat_buf.delete(doc._id);
17927
- }
17928
- return doc;
17929
- };
17930
-
17931
- // --- Rate limiter (per-process, flood-stopping) --------------------------
17932
- const _bp_hits = { ip: {}, key: {} };
17933
- const _bp_allow = function (bucket, key, max, window_ms) {
17934
- const now = Date.now();
17935
- const store = _bp_hits[bucket];
17936
- store[key] = (store[key] || []).filter((t) => now - t < window_ms);
17937
- if (store[key].length >= max) return false;
17938
- store[key].push(now);
17939
- if (Object.keys(store).length > 5000) {
17940
- for (const k of Object.keys(store)) {
17941
- if (!store[k].length || now - store[k][store[k].length - 1] > window_ms) delete store[k];
17942
- }
17943
- }
17944
- return true;
17945
- };
17946
-
17947
- // --- Proof of work -------------------------------------------------------
17948
- const _leading_zero_bits = (hex) => {
17949
- let bits = 0;
17950
- for (const ch of hex) {
17951
- const v = parseInt(ch, 16);
17952
- if (v === 0) { bits += 4; continue; }
17953
- if (v < 2) bits += 3;
17954
- else if (v < 4) bits += 2;
17955
- else if (v < 8) bits += 1;
17956
- break;
17957
- }
17958
- return bits;
17959
- };
17960
- const _pow_prefix = (session_token) => _bp_sig(`pow.${session_token}`).slice(0, 16);
17961
- const pow_make = (session_token, difficulty) => ({ prefix: _pow_prefix(session_token), difficulty });
17962
- const pow_check = (session_token, difficulty, nonce) => {
17963
- if (nonce == null) return false;
17964
- const h = crypto.createHash('sha256').update(`${_pow_prefix(session_token)}.${nonce}`).digest('hex');
17965
- return _leading_zero_bits(h) >= difficulty;
17966
- };
17967
- const _bp_pow_difficulty = (profile) => {
17968
- const d = _bp_defaults();
17969
- const base = Number(profile?.escalation?.pow_difficulty || d.pow_difficulty);
17970
- const bump = _bp_aggr(profile?.escalation?.aggressiveness || d.aggressiveness);
17971
- return Math.max(8, base + bump);
17972
- };
17973
-
17974
- // --- L0 signal scoring ---------------------------------------------------
17975
- const _bp_client_ip = (req = {}, headers = {}) => {
17976
- const h = headers || {};
17977
- const xff = h['x-forwarded-for'] || h['X-Forwarded-For'] || req.remoteip || req.ip || '';
17978
- return String(xff).split(',')[0].trim();
17979
- };
17980
- const _bp_safe_signals = (s = {}) => {
17981
- const o = {};
17982
- 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']) {
17983
- if (s[k] !== undefined) o[k] = typeof s[k] === 'string' ? String(s[k]).slice(0, 120) : s[k];
17984
- }
17985
- return o;
17986
- };
17987
- const _bp_score_signals = (signals = {}) => {
17988
- const s = signals || {};
17989
- let score = 0.5;
17990
- const reasons = [];
17991
- const inter = Number(s.pointer_events || 0) + Number(s.key_events || 0) + Number(s.scroll_events || 0);
17992
- if (inter >= 3) score += 0.25;
17993
- else if (inter === 0) { score -= 0.2; reasons.push('no_interaction'); }
17994
- if (s.has_touch === true || s.pointer_moved === true) score += 0.05;
17995
- if (s.webgl && s.canvas) score += 0.1;
17996
- else { score -= 0.1; reasons.push('no_env_hash'); }
17997
- if (s.hardware_concurrency && Number(s.hardware_concurrency) > 0) score += 0.03;
17998
- if (s.languages && String(s.languages).length) score += 0.03;
17999
- else reasons.push('no_lang');
18000
- if (s.timezone) score += 0.02;
18001
- if (s.webdriver === true) { score -= 0.5; reasons.push('webdriver'); }
18002
- if (s.headless === true) { score -= 0.4; reasons.push('headless'); }
18003
- const dwell = Number(s.dwell_ms || 0);
18004
- if (dwell >= 400) score += 0.05;
18005
- else if (dwell > 0 && dwell < 120) { score -= 0.1; reasons.push('instant_click'); }
18006
- score = Math.max(0, Math.min(1, score));
18007
- return { score, reasons };
18008
- };
18009
-
18010
- // --- L1 image challenge (from-scratch synthetic SVG tiles) ----------------
18011
- const _BP_SHAPES = ['circle', 'square', 'triangle', 'star', 'hexagon'];
18012
- const _bp_color = () => _bp_pick(['#e11d48', '#2563eb', '#16a34a', '#eab308', '#7c3aed', '#ea580c', '#0891b2', '#db2777']);
18013
- const _bp_shape_svg = (kind, color) => {
18014
- switch (kind) {
18015
- case 'circle': return `<circle cx="50" cy="50" r="30" fill="${color}"/>`;
18016
- case 'square': return `<rect x="22" y="22" width="56" height="56" rx="6" fill="${color}"/>`;
18017
- case 'triangle': return `<polygon points="50,18 82,80 18,80" fill="${color}"/>`;
18018
- 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}"/>`;
18019
- case 'hexagon': return `<polygon points="50,16 82,33 82,67 50,84 18,67 18,33" fill="${color}"/>`;
18020
- default: return '';
18021
- }
18022
- };
18023
- const _bp_tile = (kind) => {
18024
- 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>`;
18025
- return 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
18026
- };
18027
- const _image_issue = (site_key, d) => {
18028
- const grid = d.image_grid || 9;
18029
- const target = _bp_pick(_BP_SHAPES);
18030
- const others = _BP_SHAPES.filter((x) => x !== target);
18031
- const min_c = Math.max(1, d.image_correct_min || 2);
18032
- const max_c = Math.max(min_c, grid - 2);
18033
- const correct_count = min_c + Math.floor(Math.random() * (max_c - min_c + 1));
18034
- const order = [...Array(grid).keys()];
18035
- for (let i = order.length - 1; i > 0; i--) {
18036
- const j = Math.floor(Math.random() * (i + 1));
18037
- [order[i], order[j]] = [order[j], order[i]];
18038
- }
18039
- const correct = new Set(order.slice(0, correct_count));
18040
- const tiles = [];
18041
- const answer = [];
18042
- for (let i = 0; i < grid; i++) {
18043
- if (correct.has(i)) { tiles.push(_bp_tile(target)); answer.push(i); }
18044
- else tiles.push(_bp_tile(_bp_pick(others)));
18045
- }
18046
- answer.sort((a, b) => a - b);
18047
- return {
18048
- id: _bp_rand(9), kind: 'image', level: 1, site_key, ts: Date.now(), ttl: 120000,
18049
- grid, tiles, prompt: `Select every image that shows a ${target}`,
18050
- answer_hash: _bp_sha(answer.join(',')), pass_score: 0.85,
18051
- };
18052
- };
18053
- const _image_grade = (ch, answer) => {
18054
- const raw = Array.isArray(answer) ? answer : answer && Array.isArray(answer.selection) ? answer.selection : [];
18055
- const norm = [...new Set(raw.map(Number).filter((n) => Number.isInteger(n) && n >= 0 && n < ch.grid))].sort((a, b) => a - b).join(',');
18056
- return _bp_sha(norm) === ch.answer_hash;
18057
- };
18058
-
18059
- // --- L2 AI check (score, then a one-shot AI challenge) --------------------
18060
- const _bp_ip_reputation = async (ip) => {
18061
- const s = String(ip || '');
18062
- const is_private = /^(10\.|127\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|::1|fc|fd)/.test(s);
18063
- return { ip: s, private: is_private };
18064
- };
18065
- const _RiskSchema = z.object({ score: z.number(), verdict: z.enum(['human', 'bot', 'borderline']), reason: z.string() });
18066
- const _risk_score = async (profile, signals, ip) => {
18067
- try {
18068
- const rep = await _bp_ip_reputation(ip);
18069
- 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)}`;
18070
- 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 } });
18071
- if (r.code === 5 && r.data) {
18072
- const o = JSON.parse(r.data);
18073
- return { score: Number(o.score) || 0, verdict: o.verdict || 'borderline', reason: o.reason || '' };
18074
- }
18075
- } catch (e) {}
18076
- return { score: 0.5, verdict: 'borderline', reason: 'scorer_unavailable' };
18077
- };
18078
- const _bp_norm_answer = (s) => String(s || '').toLowerCase().trim().replace(/[^a-z0-9]+/g, ' ').trim();
18079
- const _DynSchema = z.object({ question: z.string(), answer: z.string() });
18080
- const _dynamic_issue = async (site_key, owner_uid) => {
18081
- try {
18082
- 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).';
18083
- 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' } });
18084
- if (r.code === 5 && r.data) {
18085
- const o = JSON.parse(r.data);
18086
- if (o.question && o.answer) {
18087
- 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 };
18088
- }
18089
- }
18090
- } catch (e) {}
18091
- return null;
18092
- };
18093
- const _dynamic_grade = async (ch, answer) => {
18094
- const a = typeof answer === 'string' ? answer : (answer && (answer.text || answer.answer)) || '';
18095
- return _bp_sha(_bp_norm_answer(a)) === ch.answer_hash;
18096
- };
18097
-
18098
- // --- Escalation orchestrator ---------------------------------------------
18099
- const _bp_challenges = new Map(); // challenge_id -> { kind, level, site_key, ts, ttl, answer_hash, ... }
18100
- const _bp_escalate = async (profile, site_key, ip, signals, level, esc, d) => {
18101
- if (level <= 1 && esc.l1_enabled !== false) {
18102
- const ch = _image_issue(site_key, d);
18103
- _bp_challenges.set(ch.id, ch);
18104
- _bp_bump_stat(profile._id, 'challenged');
18105
- return { code: 1, data: { status: 'challenge', level: 1, challenge: { id: ch.id, type: 'image', prompt: ch.prompt, tiles: ch.tiles, grid: ch.grid } } };
18106
- }
18107
- if (level <= 2 && esc.l2_enabled !== false) {
18108
- const verdict = await _risk_score(profile, signals, ip);
18109
- if (verdict.verdict === 'human' && verdict.score >= (d.l2_score_low || 0.7)) {
18110
- _bp_bump_stat(profile._id, 'passed');
18111
- 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) } };
18112
- }
18113
- if (verdict.verdict === 'bot') {
18114
- _bp_bump_stat(profile._id, 'blocked');
18115
- return { code: 1, data: { status: 'fail', level: 2, reason: 'ai_bot' } };
18116
- }
18117
- const ch = await _dynamic_issue(site_key, profile.owner_uid);
18118
- if (ch) {
18119
- _bp_challenges.set(ch.id, ch);
18120
- _bp_bump_stat(profile._id, 'challenged');
18121
- return { code: 1, data: { status: 'challenge', level: 2, challenge: { id: ch.id, type: 'ai', prompt: ch.prompt } } };
18122
- }
18123
- _bp_bump_stat(profile._id, 'blocked');
18124
- return { code: 1, data: { status: 'fail', level: 2, reason: 'ai_uncertain' } };
18125
- }
18126
- _bp_bump_stat(profile._id, 'blocked');
18127
- return { code: 1, data: { status: 'fail', level, reason: 'exhausted' } };
18128
- };
18129
-
18130
- // --- Bootstrap (broker-only; called by the router iframe + interstitial) --
18131
- export const get_captcha_bootstrap = async function (req) {
18132
- try {
18133
- const site_key = req.site_key;
18134
- const ip = _bp_client_ip(req, req.headers);
18135
- if (!site_key) return { code: -404, data: 'no_site_key' };
18136
- const profile = await _bp_resolve_by_site_key(site_key);
18137
- if (!profile || profile.enabled !== true) return { code: -404, data: 'not_enabled' };
18138
- const d = _bp_defaults();
18139
- const esc = profile.escalation || _bp_default_escalation();
18140
- const session_token = mint_clearance_token('session', `${site_key}|${ip}`, esc.session_token_ttl_ms || d.session_token_ttl_ms);
18141
- _bp_bump_stat(profile._id, 'served');
18142
- return {
18143
- code: 1,
18144
- data: {
18145
- site_key,
18146
- session_token,
18147
- pow: pow_make(session_token, _bp_pow_difficulty(profile)),
18148
- min_age_ms: d.min_age_ms,
18149
- config: {
18150
- brand: profile.brand || 'Xuda',
18151
- theme: req.theme || profile.theme || 'auto',
18152
- l1_enabled: esc.l1_enabled !== false,
18153
- l2_enabled: esc.l2_enabled !== false,
18154
- },
18155
- },
18156
- };
18157
- } catch (err) {
18158
- return { code: -404, data: err.message || String(err) };
18159
- }
18160
- };
18161
-
18162
- // --- Public: the escalation driver ---------------------------------------
18163
- export const captcha_verify = async function (req, job_id, headers) {
18164
- try {
18165
- const { site_key, session_token, signals = {}, pow_nonce, challenge_id, answer } = req;
18166
- const ip = _bp_client_ip(req, headers);
18167
- if (!site_key || !session_token) return { code: 1, data: { status: 'fail', reason: 'bad_request' } };
18168
- const profile = await _bp_resolve_by_site_key(site_key);
18169
- if (!profile || profile.enabled !== true) return { code: 1, data: { status: 'fail', reason: 'not_enabled' } };
18170
- const d = _bp_defaults();
18171
- const esc = profile.escalation || _bp_default_escalation();
18172
-
18173
- if (!_bp_allow('ip', `${site_key}|${ip}`, 60, 60000) || !_bp_allow('key', site_key, 600, 60000)) {
18174
- return { code: 1, data: { status: 'fail', reason: 'rate_limited' } };
18175
- }
18176
- const st = verify_clearance_token('session', `${site_key}|${ip}`, session_token);
18177
- if (!st.ok) return { code: 1, data: { status: 'fail', reason: `session_${st.reason}` } };
18178
-
18179
- // Answering an issued image (L1) or AI (L2) challenge.
18180
- if (challenge_id) {
18181
- const ch = _bp_challenges.get(challenge_id);
18182
- if (!ch || ch.site_key !== site_key || Date.now() - ch.ts > (ch.ttl || 120000)) {
18183
- if (ch) _bp_challenges.delete(challenge_id);
18184
- return { code: 1, data: { status: 'fail', reason: 'challenge_expired' } };
18185
- }
18186
- let ok = false;
18187
- if (ch.kind === 'image') ok = _image_grade(ch, answer);
18188
- else if (ch.kind === 'ai') ok = await _dynamic_grade(ch, answer);
18189
- _bp_challenges.delete(challenge_id);
18190
- if (ok) {
18191
- _bp_bump_stat(profile._id, 'passed');
18192
- 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) } };
18193
- }
18194
- return await _bp_escalate(profile, site_key, ip, signals, ch.level + 1, esc, d);
18195
- }
18196
-
18197
- // Level 0: honeypot + min-age + proof of work + passive signals.
18198
- if (signals && typeof signals.honeypot === 'string' && signals.honeypot.trim() !== '') {
18199
- return { code: 1, data: { status: 'fail', reason: 'ok' } };
18200
- }
18201
- if (!pow_check(session_token, _bp_pow_difficulty(profile), pow_nonce)) {
18202
- return { code: 1, data: { status: 'fail', reason: 'pow' } };
18203
- }
18204
- const min_age_ok = Date.now() - st.ts >= (d.min_age_ms || 800);
18205
- const { score, reasons } = _bp_score_signals(signals);
18206
- const aggr = _bp_aggr(esc.aggressiveness || d.aggressiveness);
18207
- if (reasons.includes('webdriver') && aggr >= 2) return { code: 1, data: { status: 'fail', reason: 'automation' } };
18208
- const pass_threshold = 0.6 + aggr * 0.1;
18209
- if (min_age_ok && score >= pass_threshold) {
18210
- _bp_bump_stat(profile._id, 'passed');
18211
- 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) } };
18212
- }
18213
- return await _bp_escalate(profile, site_key, ip, signals, 1, esc, d);
18214
- } catch (err) {
18215
- return { code: 1, data: { status: 'fail', reason: 'error' } };
18216
- }
18217
- };
18218
-
18219
- // --- Public: server-to-server verification (reCAPTCHA / Turnstile shaped) --
18220
- export const captcha_siteverify = async function (req) {
18221
- try {
18222
- const { secret, response } = req;
18223
- if (!secret || !response) return { code: 1, data: { success: false, 'error-codes': ['missing-input'] } };
18224
- const profile = await _bp_resolve_by_secret(secret);
18225
- if (!profile) return { code: 1, data: { success: false, 'error-codes': ['invalid-input-secret'] } };
18226
- const v = verify_response_token(profile.site_key, response);
18227
- if (!v.ok) return { code: 1, data: { success: false, 'error-codes': [v.reason === 'replayed' ? 'timeout-or-duplicate' : 'invalid-input-response'] } };
18228
- return { code: 1, data: { success: true, score: v.score, level_reached: v.level, hostname: profile.target?.label || '', challenge_ts: Date.now(), 'error-codes': [] } };
18229
- } catch (err) {
18230
- return { code: 1, data: { success: false, 'error-codes': ['internal-error'] } };
18231
- }
18232
- };
18233
-
18234
- // --- Entitlement (member-first; the Stripe attach lands in stripe_module) --
18235
- // Entitlement gate. Standard service is FREE for paid members; non-members pay
18236
- // $1/mo. Custom rules are a $5/mo add-on for EVERYONE (members included). Both
18237
- // tiers share the consolidated-subscription category 'bot_protection' (one line
18238
- // item, price-swapped between $1 and $5, exactly like ai_workspace tiers), so a
18239
- // custom subscriber is never double-billed the standard price. On first use we
18240
- // attach/swap the item via stripe_module; it returns -402 gracefully until the
18241
- // live Stripe prices replace the config placeholders, so nothing hard-fails.
18242
- const _bp_ensure_entitled = async (uid, tier) => {
18243
- try {
18244
- const r = await db_module.get_couch_doc('xuda_accounts', uid, true);
18245
- const acct = r && r.code > 0 && r.data ? r.data : {};
18246
- const is_member = !!(acct.membership_plan && acct.membership_plan !== 'free');
18247
- const has_item = !!acct?.stripe_subscription_items?.bot_protection;
18248
- if (tier !== 'custom') {
18249
- if (is_member) return { ok: true, member: true };
18250
- if (has_item) return { ok: true };
18251
- }
18252
- // Not entitled yet: attach the add-on (or swap the existing item to the
18253
- // custom price) on the account's consolidated subscription.
18254
- const plan_id = tier === 'custom' ? 'bot_protection_custom' : 'bot_protection';
18255
- let attach = null;
18256
- try { attach = await stripe_ms.add_subscription_item({ uid, plan_id }); } catch (e) {}
18257
- if (attach && attach.code === 1) return { ok: true };
18258
- return { ok: false, tier, price: tier === 'custom' ? 5 : 1, reason: attach && attach.data };
18259
- } catch (e) {
18260
- return { ok: false, tier, price: tier === 'custom' ? 5 : 1 };
18261
- }
18262
- };
18263
-
18264
- // --- Config sanitizers ----------------------------------------------------
18265
- const _bp_sanitize_escalation = (e) => {
18266
- const out = {};
18267
- if (['low', 'medium', 'high'].includes(e.aggressiveness)) out.aggressiveness = e.aggressiveness;
18268
- if (Number.isFinite(Number(e.pow_difficulty))) out.pow_difficulty = Math.max(8, Math.min(24, Math.round(Number(e.pow_difficulty))));
18269
- 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))));
18270
- if (typeof e.l1_enabled === 'boolean') out.l1_enabled = e.l1_enabled;
18271
- if (typeof e.l2_enabled === 'boolean') out.l2_enabled = e.l2_enabled;
18272
- return out;
18273
- };
18274
- 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) : []);
18275
- export const bp_sanitize_rules = function (rules = {}) {
18276
- const r = rules || {};
18277
- const out = {};
18278
- if (r.geo && typeof r.geo === 'object') {
18279
- out.geo = {
18280
- mode: ['off', 'challenge', 'block', 'allow'].includes(r.geo.mode) ? r.geo.mode : 'off',
18281
- countries: _bp_str_arr(r.geo.countries),
18282
- regions: _bp_str_arr(r.geo.regions),
18283
- };
18284
- }
18285
- 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);
18286
- if (win(r.hours)) out.hours = win(r.hours);
18287
- if (win(r.days)) out.days = win(r.days);
18288
- 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);
18289
- 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' };
18290
- 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 };
18291
- out.allow_list = _bp_str_arr(r.allow_list);
18292
- out.block_list = _bp_str_arr(r.block_list);
18293
- if (typeof r.require_referer === 'boolean') out.require_referer = r.require_referer;
18294
- 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);
18295
- if (['low', 'medium', 'high'].includes(r.aggressiveness)) out.aggressiveness = r.aggressiveness;
18296
- 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))));
18297
- return out;
18298
- };
18299
-
18300
- // --- Owner settings (profile setup Bot Protection tab) -------------------
18301
- const _build_captcha_snippet = (site_key, config = {}) => {
18302
- const origin = embed_origin();
18303
- const theme = config.theme && /^[a-z]+$/.test(config.theme) ? ` data-theme="${config.theme}"` : '';
18304
- return `<script src="${origin}/dist/runtime/js/captcha-loader.js" async data-sitekey="${site_key}"${theme}></script>`;
18305
- };
18306
- const _build_captcha_container = (site_key) => `<div class="xuda-captcha" data-sitekey="${site_key}"></div>`;
18307
-
18308
- export const get_bot_protection_settings = async function (req) {
18309
- try {
18310
- const { uid, profile_id } = req;
18311
- const info = await get_active_account_profile_info(uid, profile_id);
18312
- if (info.uid !== uid) return { code: -403, data: 'not_profile_owner' };
18313
- const bp_id = _bp_profile_id(uid, info.account_profile_id);
18314
- const doc = await _bp_load(bp_id);
18315
- const site_key = doc?.site_key || '';
18316
- const esc = doc?.escalation || _bp_default_escalation();
18317
- return {
18318
- code: 1,
18319
- data: {
18320
- enabled: doc?.enabled === true,
18321
- plan_tier: doc?.plan_tier || 'standard',
18322
- site_key,
18323
- secret_key: doc?.secret_key || '',
18324
- escalation: {
18325
- aggressiveness: esc.aggressiveness,
18326
- pow_difficulty: esc.pow_difficulty,
18327
- clearance_ttl_ms: esc.clearance_ttl_ms,
18328
- l1_enabled: esc.l1_enabled !== false,
18329
- l2_enabled: esc.l2_enabled !== false,
18330
- },
18331
- custom_rules: doc?.custom_rules || {},
18332
- require_on_contact_form: doc?.require_on_contact_form === true,
18333
- widget_profile_id: `${uid}.${info.account_profile_id}`,
18334
- iframe_url: site_key ? `${embed_origin()}/captcha/${site_key}` : '',
18335
- snippet: site_key ? _build_captcha_snippet(site_key, { theme: doc?.theme }) : '',
18336
- container_snippet: site_key ? _build_captcha_container(site_key) : '',
18337
- },
18338
- };
18339
- } catch (err) {
18340
- return { code: -1, data: err.message || String(err) };
18341
- }
18342
- };
18343
-
18344
- export const update_bot_protection_settings = async function (req) {
18345
- try {
18346
- const { uid, profile_id, enabled, escalation, custom_rules, require_on_contact_form, rotate_secret } = req;
18347
- const info = await get_active_account_profile_info(uid, profile_id);
18348
- if (info.uid !== uid) return { code: -403, data: 'not_profile_owner' };
18349
- const doc = await _bp_ensure_profile(uid, info.account_profile_id, { surface: 'widget' });
18350
-
18351
- if (enabled === true && doc.enabled !== true) {
18352
- const ent = await _bp_ensure_entitled(uid, doc.plan_tier || 'standard');
18353
- if (!ent.ok) return { code: -402, data: { error: 'billing_required', tier: ent.tier, price: ent.price } };
18354
- }
18355
- if (typeof enabled === 'boolean') doc.enabled = enabled;
18356
- if (doc.enabled && !doc.site_key) {
18357
- const k = _bp_new_keys();
18358
- doc.site_key = k.site_key;
18359
- doc.secret_key = k.secret_key;
18360
- }
18361
- if (rotate_secret === true && doc.site_key) {
18362
- if (doc.secret_key) await _bp_delete_index(_bp_index_id.secret(doc.secret_key));
18363
- doc.secret_key = _bp_new_keys().secret_key;
18364
- }
18365
- if (escalation && typeof escalation === 'object') {
18366
- doc.escalation = { ...(doc.escalation || _bp_default_escalation()), ..._bp_sanitize_escalation(escalation) };
18367
- }
18368
- if (custom_rules && typeof custom_rules === 'object') doc.custom_rules = bp_sanitize_rules(custom_rules);
18369
- if (typeof require_on_contact_form === 'boolean') doc.require_on_contact_form = require_on_contact_form;
18370
- _bp_flush_stats(doc);
18371
- await _bp_save(doc);
18372
- return await get_bot_protection_settings({ uid, profile_id });
18373
- } catch (err) {
18374
- return { code: -1, data: err.message || String(err) };
18375
- }
18376
- };
18377
-
18378
- ///////////////////////////////////////////////////////////////////////////////
18379
- // BOT PROTECTION - Surface 2 lifecycle (origin protection of a target).
18380
- // These are called by app_module's thin cpi handlers over the broker (uid is
18381
- // passed explicitly, already authenticated at the http layer), plus two
18382
- // broker-only helpers the router uses at the edge. See docs/handoff_bot_protection_ishai.md.
18383
- ///////////////////////////////////////////////////////////////////////////////
18384
-
18385
- // Stable per-target slug so re-attaching the same target maps to one profile.
18386
- const _bp_slug_for_target = (target) => 't_' + _bp_sha(String((target && target.id) || '')).slice(0, 16);
18387
- const _bp_summary = (doc) => ({
18388
- profile_id: doc._id,
18389
- target: doc.target || null,
18390
- enabled: doc.enabled === true,
18391
- armed: doc.armed === true,
18392
- plan_tier: doc.plan_tier || 'standard',
18393
- surface: doc.surface || 'origin',
18394
- site_key: doc.site_key || '',
18395
- stats: _bp_merged_stats(doc),
18396
- });
18397
-
18398
- // IPv4 CIDR membership test (dependency-free) for allow/block-list rules.
18399
- const _cidr_match = (cidr, ip) => {
18400
- try {
18401
- if (typeof cidr !== 'string' || cidr.indexOf('/') < 0) return false;
18402
- const [range, bitsStr] = cidr.split('/');
18403
- const bits = parseInt(bitsStr, 10);
18404
- if (!/^\d+\.\d+\.\d+\.\d+$/.test(range) || !/^\d+\.\d+\.\d+\.\d+$/.test(ip) || !(bits >= 0 && bits <= 32)) return false;
18405
- const toInt = (a) => a.split('.').reduce((s, o) => ((s << 8) + (parseInt(o, 10) & 255)) >>> 0, 0) >>> 0;
18406
- const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0;
18407
- return (toInt(range) & mask) === (toInt(ip) & mask);
18408
- } catch (e) {
18409
- return false;
18410
- }
18411
- };
18412
-
18413
- export const bp_attach = async function (req) {
18414
- try {
18415
- const { uid, target, plan_tier, escalation } = req;
18416
- if (!uid || !target || !target.id) return { code: -1, data: 'bad_request' };
18417
- const tier = plan_tier === 'custom' ? 'custom' : 'standard';
18418
- const ent = await _bp_ensure_entitled(uid, tier);
18419
- if (!ent.ok) return { code: -402, data: { error: 'billing_required', tier: ent.tier, price: ent.price } };
18420
- const slug = _bp_slug_for_target(target);
18421
- const doc = await _bp_ensure_profile(uid, slug, { surface: 'origin', plan_tier: tier, target });
18422
- doc.target = target;
18423
- doc.plan_tier = tier;
18424
- doc.surface = doc.surface === 'widget' ? 'both' : 'origin';
18425
- doc.enabled = true;
18426
- if (!doc.site_key) { const k = _bp_new_keys(); doc.site_key = k.site_key; doc.secret_key = k.secret_key; }
18427
- if (escalation && typeof escalation === 'object') doc.escalation = { ...(doc.escalation || _bp_default_escalation()), ..._bp_sanitize_escalation(escalation) };
18428
- _bp_flush_stats(doc);
18429
- await _bp_save(doc);
18430
- return { code: 1, data: _bp_summary(doc) };
18431
- } catch (err) {
18432
- return { code: -1, data: err.message || String(err) };
18433
- }
18434
- };
18435
-
18436
- export const bp_get_profiles = async function (req) {
18437
- try {
18438
- const { uid } = req;
18439
- if (!uid) return { code: -1, data: 'no uid' };
18440
- const q = await db_module.find_couch_query(BP_DB, { selector: { docType: 'bot_protection_profile', owner_uid: uid }, limit: 500 }, true, true);
18441
- const docs = q?.docs || [];
18442
- return { code: 1, data: { profiles: docs.map(_bp_summary) } };
18443
- } catch (err) {
18444
- return { code: -1, data: err.message || String(err) };
18445
- }
18446
- };
18447
-
18448
- export const bp_arm = async function (req) {
18449
- try {
18450
- const { uid, profile_id, app_id, armed, target } = req;
18451
- if (!uid) return { code: -1, data: 'no uid' };
18452
- let doc = null;
18453
- if (profile_id) {
18454
- doc = await _bp_load(profile_id);
18455
- } else if (target && target.id) {
18456
- doc = await _bp_ensure_profile(uid, _bp_slug_for_target(target), { surface: 'origin', target });
18457
- if (!doc.target) doc.target = target;
18458
- } else if (app_id) {
18459
- const slug = _bp_slug_for_target({ id: app_id });
18460
- doc = await _bp_load(_bp_profile_id(uid, slug));
18461
- if (!doc) doc = await _bp_ensure_profile(uid, slug, { surface: 'origin', target: { type: 'internal', kind: 'app', id: app_id, label: app_id, region: null } });
18462
- }
18463
- if (!doc) return { code: -404, data: 'profile_not_found' };
18464
- if (doc.owner_uid !== uid) return { code: -403, data: 'not_owner' };
18465
- const want = armed === true;
18466
- if (want) {
18467
- const ent = await _bp_ensure_entitled(uid, doc.plan_tier || 'standard');
18468
- if (!ent.ok) return { code: -402, data: { error: 'billing_required', tier: ent.tier, price: ent.price } };
18469
- if (!doc.site_key) { const k = _bp_new_keys(); doc.site_key = k.site_key; doc.secret_key = k.secret_key; }
18470
- doc.enabled = true;
18471
- }
18472
- doc.armed = want;
18473
- _bp_flush_stats(doc);
18474
- await _bp_save(doc);
18475
- return { code: 1, data: _bp_summary(doc) };
18476
- } catch (err) {
18477
- return { code: -1, data: err.message || String(err) };
18478
- }
18479
- };
18480
-
18481
- export const bp_detach = async function (req) {
18482
- try {
18483
- const { uid, profile_id } = req;
18484
- const doc = await _bp_load(profile_id);
18485
- if (!doc) return { code: 1, data: { ok: true } };
18486
- if (doc.owner_uid !== uid) return { code: -403, data: 'not_owner' };
18487
- if (doc.site_key) await _bp_delete_index(_bp_index_id.key(doc.site_key));
18488
- if (doc.secret_key) await _bp_delete_index(_bp_index_id.secret(doc.secret_key));
18489
- const tkey = _bp_target_key(doc);
18490
- if (tkey) await _bp_delete_index(_bp_index_id.target(tkey));
18491
- try {
18492
- const r = await db_module.get_couch_doc(BP_DB, doc._id, true);
18493
- if (r?.data?._rev) await db_module.delete_couch_doc(BP_DB, doc._id, r.data._rev);
18494
- } catch (e) {}
18495
- _bp_stat_buf.delete(doc._id);
18496
- return { code: 1, data: { ok: true } };
18497
- } catch (err) {
18498
- return { code: -1, data: err.message || String(err) };
18499
- }
18500
- };
18501
-
18502
- export const bp_set_custom_rules = async function (req) {
18503
- try {
18504
- const { uid, profile_id, rules } = req;
18505
- const doc = await _bp_load(profile_id);
18506
- if (!doc) return { code: -404, data: 'profile_not_found' };
18507
- if (doc.owner_uid !== uid) return { code: -403, data: 'not_owner' };
18508
- const ent = await _bp_ensure_entitled(uid, 'custom');
18509
- if (!ent.ok) return { code: -402, data: { error: 'billing_required', tier: 'custom', price: 5 } };
18510
- doc.plan_tier = 'custom';
18511
- doc.custom_rules = bp_sanitize_rules(rules || {});
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
- // Broker-only: the router resolves the guarding profile for a target at the edge.
18521
- export const bp_resolve_target = async function (req) {
18522
- try {
18523
- const { target_key } = req;
18524
- if (!target_key) return { code: -404, data: 'no_target' };
18525
- const idx = await _bp_load_index(_bp_index_id.target(target_key));
18526
- if (!idx) return { code: -404, data: 'not_found' };
18527
- const doc = await _bp_load(idx.profile_id);
18528
- if (!doc) return { code: -404, data: 'not_found' };
18529
- return {
18530
- code: 1,
18531
- data: {
18532
- profile_id: doc._id,
18533
- armed: doc.armed === true,
18534
- enabled: doc.enabled === true,
18535
- plan_tier: doc.plan_tier || 'standard',
18536
- site_key: doc.site_key || '',
18537
- custom_rules: doc.custom_rules || {},
18538
- escalation: doc.escalation || _bp_default_escalation(),
18539
- target: doc.target || null,
18540
- },
18541
- };
18542
- } catch (err) {
18543
- return { code: -404, data: err.message || String(err) };
18544
- }
18545
- };
18546
-
18547
- // Broker-only: evaluate the custom rules for an inbound request context and
18548
- // return the enforcement action. Standard tier ignores custom_rules (challenge
18549
- // when armed); custom tier honors the full rule set.
18550
- export const evaluate_custom_rules = async function (req) {
18551
- try {
18552
- const { profile, ctx } = req;
18553
- const d = _bp_defaults();
18554
- const esc = (profile && profile.escalation) || _bp_default_escalation();
18555
- const out = { action: 'challenge', start_level: 0, aggressiveness: esc.aggressiveness || d.aggressiveness, clearance_ttl_ms: esc.clearance_ttl_ms || d.clearance_ttl_ms };
18556
- const c = ctx || {};
18557
- const rules = profile && profile.plan_tier === 'custom' && profile.custom_rules ? profile.custom_rules : null;
18558
- const inList = (list, ip, country) => (list || []).some((e) => e === ip || e === country || _cidr_match(e, ip));
18559
- if (rules) {
18560
- if (inList(rules.block_list, c.ip, c.country)) return { code: 1, data: { ...out, action: 'block' } };
18561
- if (inList(rules.allow_list, c.ip, c.country)) return { code: 1, data: { ...out, action: 'allow' } };
18562
- if (rules.reputation && rules.reputation.allow_verified_search_bots && c.verified_bot) return { code: 1, data: { ...out, action: 'allow' } };
18563
- if (rules.geo && rules.geo.mode && rules.geo.mode !== 'off' && c.country && (rules.geo.countries || []).includes(c.country)) {
18564
- if (rules.geo.mode === 'block') return { code: 1, data: { ...out, action: 'block' } };
18565
- if (rules.geo.mode === 'allow') return { code: 1, data: { ...out, action: 'allow' } };
18566
- out.action = 'challenge';
18567
- }
18568
- if (Array.isArray(rules.paths) && c.path) {
18569
- for (const p of rules.paths) {
18570
- if (p && p.pattern && c.path.indexOf(p.pattern) === 0) {
18571
- if (p.action === 'block') return { code: 1, data: { ...out, action: 'block' } };
18572
- if (p.action === 'allow') return { code: 1, data: { ...out, action: 'allow' } };
18573
- out.action = 'challenge';
18574
- break;
18575
- }
18576
- }
18577
- }
18578
- 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';
18579
- 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';
18580
- if (rules.require_referer && !c.has_referer) out.action = 'challenge';
18581
- if (['low', 'medium', 'high'].includes(rules.aggressiveness)) out.aggressiveness = rules.aggressiveness;
18582
- if (Number.isFinite(Number(rules.clearance_ttl_ms))) out.clearance_ttl_ms = Number(rules.clearance_ttl_ms);
18583
- }
18584
- return { code: 1, data: out };
18585
- } catch (err) {
18586
- return { code: 1, data: { action: 'challenge', start_level: 0 } };
18587
- }
18588
- };
18589
-
18590
- // Broker-only: the origin interstitial passes a captcha response token (from the
18591
- // embedded widget) here. Resolves the profile by site_key, verifies and burns the
18592
- // token, and returns the guarded target so the router can mint a clearance cookie.
18593
- export const bp_consume_response = async function (req) {
18594
- try {
18595
- const { site_key, response } = req;
18596
- const profile = await _bp_resolve_by_site_key(site_key);
18597
- if (!profile || profile.enabled !== true) return { code: 1, data: { ok: false, reason: 'unknown_site' } };
18598
- const v = verify_response_token(profile.site_key, response);
18599
- if (!v.ok) return { code: 1, data: { ok: false, reason: v.reason } };
18600
- const esc = profile.escalation || _bp_default_escalation();
18601
- const d = _bp_defaults();
18602
- 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 } };
18603
- } catch (err) {
18604
- return { code: 1, data: { ok: false, reason: 'error' } };
18605
- }
18606
- };
18607
-
18608
- // Broker-only: the router caches the set of armed targets (refreshed periodically)
18609
- // so the enforcement hot path is a local lookup and unguarded traffic never hits
18610
- // the broker.
18611
- export const bp_armed_targets = async function () {
18612
- try {
18613
- 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);
18614
- 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);
18615
- return { code: 1, data: rows };
18616
- } catch (err) {
18617
- return { code: -1, data: err.message || String(err) };
18618
- }
18619
- };
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
  };
@@ -241,6 +245,10 @@ export const is_business_contact_has_person = async function (...args) {
241
245
  return await broker.send_to_queue("is_business_contact_has_person", ...args);
242
246
  };
243
247
 
248
+ export const inspect_profile_picture = async function (...args) {
249
+ return await broker.send_to_queue("inspect_profile_picture", ...args);
250
+ };
251
+
244
252
  export const get_name_from_email_addr = async function (...args) {
245
253
  return await broker.send_to_queue("get_name_from_email_addr", ...args);
246
254
  };
@@ -356,71 +364,3 @@ export const get_contact_form_bootstrap = async function (...args) {
356
364
  export const contact_form_submit = async function (...args) {
357
365
  return await broker.send_to_queue("contact_form_submit", ...args);
358
366
  };
359
-
360
- export const mint_clearance_token = async function (...args) {
361
- return await broker.send_to_queue("mint_clearance_token", ...args);
362
- };
363
-
364
- export const verify_clearance_token = async function (...args) {
365
- return await broker.send_to_queue("verify_clearance_token", ...args);
366
- };
367
-
368
- export const get_captcha_bootstrap = async function (...args) {
369
- return await broker.send_to_queue("get_captcha_bootstrap", ...args);
370
- };
371
-
372
- export const captcha_verify = async function (...args) {
373
- return await broker.send_to_queue("captcha_verify", ...args);
374
- };
375
-
376
- export const captcha_siteverify = async function (...args) {
377
- return await broker.send_to_queue("captcha_siteverify", ...args);
378
- };
379
-
380
- export const bp_sanitize_rules = async function (...args) {
381
- return await broker.send_to_queue("bp_sanitize_rules", ...args);
382
- };
383
-
384
- export const get_bot_protection_settings = async function (...args) {
385
- return await broker.send_to_queue("get_bot_protection_settings", ...args);
386
- };
387
-
388
- export const update_bot_protection_settings = async function (...args) {
389
- return await broker.send_to_queue("update_bot_protection_settings", ...args);
390
- };
391
-
392
- export const bp_attach = async function (...args) {
393
- return await broker.send_to_queue("bp_attach", ...args);
394
- };
395
-
396
- export const bp_get_profiles = async function (...args) {
397
- return await broker.send_to_queue("bp_get_profiles", ...args);
398
- };
399
-
400
- export const bp_arm = async function (...args) {
401
- return await broker.send_to_queue("bp_arm", ...args);
402
- };
403
-
404
- export const bp_detach = async function (...args) {
405
- return await broker.send_to_queue("bp_detach", ...args);
406
- };
407
-
408
- export const bp_set_custom_rules = async function (...args) {
409
- return await broker.send_to_queue("bp_set_custom_rules", ...args);
410
- };
411
-
412
- export const bp_resolve_target = async function (...args) {
413
- return await broker.send_to_queue("bp_resolve_target", ...args);
414
- };
415
-
416
- export const evaluate_custom_rules = async function (...args) {
417
- return await broker.send_to_queue("evaluate_custom_rules", ...args);
418
- };
419
-
420
- export const bp_consume_response = async function (...args) {
421
- return await broker.send_to_queue("bp_consume_response", ...args);
422
- };
423
-
424
- export const bp_armed_targets = async function (...args) {
425
- return await broker.send_to_queue("bp_armed_targets", ...args);
426
- };
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
  };
@@ -241,6 +245,10 @@ export const is_business_contact_has_person = function (...args) {
241
245
  broker.send_to_queue_async("is_business_contact_has_person", ...args);
242
246
  };
243
247
 
248
+ export const inspect_profile_picture = function (...args) {
249
+ broker.send_to_queue_async("inspect_profile_picture", ...args);
250
+ };
251
+
244
252
  export const get_name_from_email_addr = function (...args) {
245
253
  broker.send_to_queue_async("get_name_from_email_addr", ...args);
246
254
  };
@@ -356,71 +364,3 @@ export const get_contact_form_bootstrap = function (...args) {
356
364
  export const contact_form_submit = function (...args) {
357
365
  broker.send_to_queue_async("contact_form_submit", ...args);
358
366
  };
359
-
360
- export const mint_clearance_token = function (...args) {
361
- broker.send_to_queue_async("mint_clearance_token", ...args);
362
- };
363
-
364
- export const verify_clearance_token = function (...args) {
365
- broker.send_to_queue_async("verify_clearance_token", ...args);
366
- };
367
-
368
- export const get_captcha_bootstrap = function (...args) {
369
- broker.send_to_queue_async("get_captcha_bootstrap", ...args);
370
- };
371
-
372
- export const captcha_verify = function (...args) {
373
- broker.send_to_queue_async("captcha_verify", ...args);
374
- };
375
-
376
- export const captcha_siteverify = function (...args) {
377
- broker.send_to_queue_async("captcha_siteverify", ...args);
378
- };
379
-
380
- export const bp_sanitize_rules = function (...args) {
381
- broker.send_to_queue_async("bp_sanitize_rules", ...args);
382
- };
383
-
384
- export const get_bot_protection_settings = function (...args) {
385
- broker.send_to_queue_async("get_bot_protection_settings", ...args);
386
- };
387
-
388
- export const update_bot_protection_settings = function (...args) {
389
- broker.send_to_queue_async("update_bot_protection_settings", ...args);
390
- };
391
-
392
- export const bp_attach = function (...args) {
393
- broker.send_to_queue_async("bp_attach", ...args);
394
- };
395
-
396
- export const bp_get_profiles = function (...args) {
397
- broker.send_to_queue_async("bp_get_profiles", ...args);
398
- };
399
-
400
- export const bp_arm = function (...args) {
401
- broker.send_to_queue_async("bp_arm", ...args);
402
- };
403
-
404
- export const bp_detach = function (...args) {
405
- broker.send_to_queue_async("bp_detach", ...args);
406
- };
407
-
408
- export const bp_set_custom_rules = function (...args) {
409
- broker.send_to_queue_async("bp_set_custom_rules", ...args);
410
- };
411
-
412
- export const bp_resolve_target = function (...args) {
413
- broker.send_to_queue_async("bp_resolve_target", ...args);
414
- };
415
-
416
- export const evaluate_custom_rules = function (...args) {
417
- broker.send_to_queue_async("evaluate_custom_rules", ...args);
418
- };
419
-
420
- export const bp_consume_response = function (...args) {
421
- broker.send_to_queue_async("bp_consume_response", ...args);
422
- };
423
-
424
- export const bp_armed_targets = function (...args) {
425
- broker.send_to_queue_async("bp_armed_targets", ...args);
426
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.5646",
3
+ "version": "1.1.5648",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",