@xuda.io/ai_module 1.1.5634 → 1.1.5635

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
@@ -161,13 +161,49 @@ const get_openai_codex_cli_command = function () {
161
161
  return _conf.openai_codex_command || 'codex';
162
162
  };
163
163
 
164
+ // Cheapest text model in the catalog (lowest input price). Used as the DEFAULT codex
165
+ // model when the user hasn't picked one. Returns a catalog CODE (e.g. 'gpt-1').
166
+ const get_cheapest_ai_model_code = function () {
167
+ const models = _conf?.ai_models || {};
168
+ let best = null;
169
+ for (const [code, e] of Object.entries(models)) {
170
+ if (!e || e.type !== 'text') continue;
171
+ const p = Number(e.input);
172
+ if (!Number.isFinite(p)) continue;
173
+ if (!best || p < best.p) best = { code, p };
174
+ }
175
+ return best?.code || _conf.default_ai_model || null;
176
+ };
177
+
164
178
  const get_openai_codex_model = function () {
165
- return _conf.openai_codex_model || _conf.ai_models?.openai_codex_model?.model || null;
179
+ // Codex is user-selectable per request (req.codex_model); when nothing is picked
180
+ // the default is the CHEAPEST catalog model. Returns a catalog code (resolved to a
181
+ // real OpenAI id at exec time by get_openai_codex_exec_args).
182
+ return _conf.openai_codex_model || get_cheapest_ai_model_code();
183
+ };
184
+
185
+ // Map a legacy ai_models key still stored on old profiles/agents (e.g. 'gpt-5.4') to
186
+ // its current catalog code (e.g. 'gpt-4') via _conf.ai_model_aliases. Current codes
187
+ // and unknown ids pass through. This makes the gpt-1..gpt-5 recode backward
188
+ // compatible with no data migration.
189
+ const canonical_ai_code = function (m) {
190
+ return _conf?.ai_model_aliases?.[m] || m;
191
+ };
192
+
193
+ // Resolve a catalog code (the stable key that profiles/agents store, e.g.
194
+ // active_ai_model / agentConfig.agent_ai_model) to the real OpenAI model id to send
195
+ // to the API. Applies the legacy-key alias first, so old stored values keep working
196
+ // after a recode. Falls back to the input for literal ids that aren't catalog keys
197
+ // (e.g. 'gpt-image-1'). Apply ONLY at the OpenAI/Agent/Codex-CLI call boundary:
198
+ // billing (record_ai_usage) and the model-picker validation must keep the code.
199
+ const resolve_ai_model = function (m) {
200
+ const code = canonical_ai_code(m);
201
+ return _conf?.ai_models?.[code]?.model || _conf?.ai_models?.[m]?.model || m;
166
202
  };
167
203
 
168
204
  const get_openai_codex_usage_model = function (codex_model, requested_codex_model) {
169
- if (requested_codex_model) return codex_model;
170
- return _conf.ai_models?.openai_codex_model ? 'openai_codex_model' : codex_model || _conf.openai_codex_command || 'openai_codex_cli';
205
+ // Bill by the catalog code actually used (requested selection or cheapest default).
206
+ return codex_model || _conf.openai_codex_command || 'openai_codex_cli';
171
207
  };
172
208
 
173
209
  const get_openai_codex_exec_args = function (codex_model, opts = {}) {
@@ -180,7 +216,9 @@ const get_openai_codex_exec_args = function (codex_model, opts = {}) {
180
216
  const bypass_approvals_and_sandbox = opts.sandbox ? false : typeof _conf.openai_codex_bypass_approvals_and_sandbox === 'undefined' ? true : _conf.openai_codex_bypass_approvals_and_sandbox;
181
217
 
182
218
  if (codex_model) {
183
- args.push('--model', codex_model);
219
+ // codex_model is a catalog code (user pick or cheapest default); the Codex CLI
220
+ // needs the real OpenAI id.
221
+ args.push('--model', resolve_ai_model(codex_model));
184
222
  }
185
223
 
186
224
  if (bypass_approvals_and_sandbox) {
@@ -2291,7 +2329,7 @@ const create_image = async function (uid, prompt, model = 'gpt-image-1-mini', si
2291
2329
  let response;
2292
2330
  try {
2293
2331
  response = await client.images.generate({
2294
- model,
2332
+ model: resolve_ai_model(model),
2295
2333
  prompt,
2296
2334
  size,
2297
2335
  n,
@@ -4312,7 +4350,7 @@ export const submit_chat_gpt_prompt = async function (req) {
4312
4350
  let response;
4313
4351
  try {
4314
4352
  let opt = {
4315
- model,
4353
+ model: resolve_ai_model(model),
4316
4354
  input: prompt,
4317
4355
  tools,
4318
4356
  text: {
@@ -4893,6 +4931,11 @@ export const submit_chat_conversation = async function (req, job_id, headers) {
4893
4931
  break;
4894
4932
  }
4895
4933
 
4934
+ case 'ticket': {
4935
+ ret = await contact_ticket_conversation(req, job_id, headers);
4936
+ break;
4937
+ }
4938
+
4896
4939
  default:
4897
4940
  ret = { code: -44, data: 'invalid conversation_type ' + conversation_doc.conversation_type };
4898
4941
  break;
@@ -5152,7 +5195,24 @@ const chat_email = async function (req, job_id, headers) {
5152
5195
  email_attachments.push(await save_drive_file_to_disk(attachment));
5153
5196
  }
5154
5197
 
5155
- sent_email_result = await email_ms.sendEmailFromAccount(email_account_doc, contact_info.email, subject, body, null, email_attachments);
5198
+ // Wrap the body with the profile's selected email template (Email
5199
+ // Template tab). 'plain' or an unset style renders null, keeping the
5200
+ // original raw-text behavior.
5201
+ let template_html = null;
5202
+ try {
5203
+ const profile_doc = account_profile_info.account_profile_obj || {};
5204
+ const render_ret = await email_ms.render_profile_email({
5205
+ style: profile_doc.email_template?.style,
5206
+ body_text: body,
5207
+ profile_name: profile_doc.profile_name,
5208
+ signature: profile_doc.profile_signature,
5209
+ avatar_url: profile_doc.profile_picture || profile_doc.profile_avatar || '',
5210
+ brand_color: profile_doc.widget_config?.color || '',
5211
+ });
5212
+ if (render_ret?.code > 0 && render_ret.data) template_html = render_ret.data;
5213
+ } catch (err) {}
5214
+
5215
+ sent_email_result = await email_ms.sendEmailFromAccount(email_account_doc, contact_info.email, subject, body, template_html, email_attachments);
5156
5216
  if (!sent_email_result.success) {
5157
5217
  throw new Error('error sending email');
5158
5218
  }
@@ -5703,6 +5763,22 @@ const resolve_plugin_action = function (req, dashboard_context, prompt) {
5703
5763
  // answers are specific to THIS project instead of generic. Terse on purpose to stay
5704
5764
  // within the token budget; the snapshot is trusted for grounding only — the agent
5705
5765
  // still calls CPI tools (which re-check ownership) for any actual read/write.
5766
+ // The 'ask_ai' access consent ("let the assistant read this server's current status").
5767
+ // Gated on the customer's own machines only: app_type 'vps' (full-stack is vps +
5768
+ // is_full_stack, so it is covered) and 'external_vps'. MIRRORS
5769
+ // app_module.CONSENT_ENFORCED_APP_TYPES — no cpi module imports app_module, so the
5770
+ // predicate is duplicated here on purpose; keep the copies in step.
5771
+ //
5772
+ // Unlike files/metrics/backup this does NOT return -91. This builder never touches the
5773
+ // server — it only copies facts off the already-loaded app doc into the system prompt —
5774
+ // and the chat does far more than answer server questions, so blocking it would be
5775
+ // wrong. Enforcement here is REDACTION: no consent → the assistant simply is not told
5776
+ // the server's address/status. Anything that actually reads the box goes through the
5777
+ // CPI tools, which carry their own gates (vps_fs_* → 'files').
5778
+ const _ASK_AI_CONSENT_TYPES = ['vps', 'external_vps'];
5779
+ const _ask_ai_consent_blocked = (o) =>
5780
+ !!o && _ASK_AI_CONSENT_TYPES.includes(o.app_type) && o?.deploy_data?.access_consents?.ask_ai?.granted !== true;
5781
+
5706
5782
  const build_dashboard_state_context = function (app_obj, ctx) {
5707
5783
  const lines = [];
5708
5784
  if (app_obj) {
@@ -5712,7 +5788,7 @@ const build_dashboard_state_context = function (app_obj, ctx) {
5712
5788
  const plan = app_obj.app_uId_plan || app_obj.membership_plan;
5713
5789
  if (plan) lines.push(`- Plan: ${plan}`);
5714
5790
  const vps = app_obj.app_hosting_server;
5715
- if (vps && (vps.ip || vps.droplet?.id)) {
5791
+ if (vps && (vps.ip || vps.droplet?.id) && !_ask_ai_consent_blocked(app_obj)) {
5716
5792
  lines.push(`- VPS: ${vps.ip || 'provisioned'}${vps.droplet?.status ? ` (${vps.droplet.status})` : ''}${vps.region ? `, ${vps.region}` : ''}`);
5717
5793
  }
5718
5794
  const sw = app_obj.static_website;
@@ -5746,7 +5822,7 @@ const build_dashboard_state_context = function (app_obj, ctx) {
5746
5822
  const pick_dashboard_model = function (prompt, dashboard_context) {
5747
5823
  const models = _conf.ai_models || {};
5748
5824
  const fallback = _conf.default_ai_model;
5749
- const strong = (_conf.strong_ai_model && models[_conf.strong_ai_model] && _conf.strong_ai_model) || (models['gpt-5.4-mini'] && 'gpt-5.4-mini') || fallback;
5825
+ const strong = (_conf.strong_ai_model && models[_conf.strong_ai_model] && _conf.strong_ai_model) || (models['gpt-3'] && 'gpt-3') || fallback;
5750
5826
  // Intent comes from the PROMPT only — folding in dashboard_context would let a
5751
5827
  // tab named with an action verb (e.g. "deploy") force every question on that
5752
5828
  // tab to the strong tier ("is my site live?" on the deploy tab is still a read).
@@ -6389,7 +6465,7 @@ ${CLARIFYING_QUESTIONS_INSTRUCTION}
6389
6465
  Tab/context: "${dashboard_context || 'unknown'}" · app_id: "${target_app_id}"
6390
6466
  Available CPI methods: ${cpi_tools_ret.selected_methods.join(', ')}${dashboard_state_block ? '\n\n' + dashboard_state_block : ''}${action_steer ? '\n\n' + action_steer : ''}
6391
6467
  `.trim(),
6392
- model: ai_model,
6468
+ model: resolve_ai_model(ai_model),
6393
6469
  tools: cpi_tools_ret.tools,
6394
6470
  });
6395
6471
 
@@ -7170,7 +7246,7 @@ const ai_chat_conversation = async function (req, job_id, headers) {
7170
7246
  let prompt_suggestion_activated;
7171
7247
  let chat_suggestion_activated;
7172
7248
  let model = ai_model;
7173
- if (!_conf.ai_models[model]) {
7249
+ if (!_conf.ai_models[canonical_ai_code(model)]) {
7174
7250
  throw new Error('model not supported');
7175
7251
  }
7176
7252
 
@@ -7517,7 +7593,7 @@ const ai_chat_conversation = async function (req, job_id, headers) {
7517
7593
  const agent = new Agent({
7518
7594
  name: agent_name.substring(0, 55),
7519
7595
  instructions: ai_agent_doc.agentConfig.agent_instructions + (reference_type === 'ai_agents' ? get_agent_instructions() : '') + (context?.has_full_stack_vps ? '\n\n' + get_full_stack_vps_instructions() : ''),
7520
- model,
7596
+ model: resolve_ai_model(model),
7521
7597
  tools,
7522
7598
  metadata: {
7523
7599
  agent_id: ai_agent_doc._id,
@@ -7674,7 +7750,7 @@ const ai_chat_conversation = async function (req, job_id, headers) {
7674
7750
  instructions: get_agent_instructions(),
7675
7751
  // inputGuardrails: [contactGuardrail],
7676
7752
  // outputGuardrails: [contactGuardrail],
7677
- model,
7753
+ model: resolve_ai_model(model),
7678
7754
  handoffs: agents, // this lets triageAgent forward to other agents
7679
7755
  });
7680
7756
  }
@@ -8002,7 +8078,7 @@ Do not mention that the reply is automated.
8002
8078
  ${conversation_type === 'chat' ? 'Return only the chat message text, ready to send.' : 'Return only the email body, ready to send.'}
8003
8079
  If a signature is available, append it at the end:
8004
8080
  ${account_profile_doc.profile_signature || ''}`.trim(),
8005
- model,
8081
+ model: resolve_ai_model(model),
8006
8082
  metadata: { auto_response: true, default: true, ts: Date.now() },
8007
8083
  }),
8008
8084
  );
@@ -8047,7 +8123,7 @@ If a signature is available, append it at the end:
8047
8123
  ${account_profile_doc.profile_signature || ''}`
8048
8124
  : ''
8049
8125
  }${context?.has_full_stack_vps ? '\n\n' + get_full_stack_vps_instructions() : ''}`.trim(),
8050
- model,
8126
+ model: resolve_ai_model(model),
8051
8127
  tools: tools_ret.tools,
8052
8128
  metadata: {
8053
8129
  agent_id: ai_agent_doc._id,
@@ -8075,7 +8151,7 @@ ${account_profile_doc.profile_signature || ''}`
8075
8151
  name: 'Auto Response Triage Agent',
8076
8152
  instructions: `Select the best specialist agent to draft an automatic ${conversation_type === 'chat' ? 'chat' : 'email'} response.
8077
8153
  Return only the final ${conversation_type === 'chat' ? 'chat message text' : 'email body text'}.`,
8078
- model,
8154
+ model: resolve_ai_model(model),
8079
8155
  handoffs: agents,
8080
8156
  });
8081
8157
  }
@@ -8143,6 +8219,18 @@ Return only the email body.`;
8143
8219
  undefined,
8144
8220
  undefined,
8145
8221
  );
8222
+ } else if (conversation_type === 'ticket') {
8223
+ await contact_ticket_conversation(
8224
+ {
8225
+ profile_id,
8226
+ uid,
8227
+ prompt: response_text,
8228
+ conversation_doc,
8229
+ _auto_response: true,
8230
+ },
8231
+ undefined,
8232
+ undefined,
8233
+ );
8146
8234
  } else {
8147
8235
  await chat_email(
8148
8236
  {
@@ -8792,7 +8880,7 @@ export const get_profile_avatar = async function (profile_picture, uid, prompt,
8792
8880
  let ai_avatar_response;
8793
8881
  try {
8794
8882
  ai_avatar_response = await client.images.edit({
8795
- model,
8883
+ model: resolve_ai_model(model),
8796
8884
  image: image_blob_ret.image_blob, //base photo
8797
8885
  prompt,
8798
8886
  background: 'transparent',
@@ -8903,7 +8991,7 @@ export const get_profile_avatar = async function (profile_picture, uid, prompt,
8903
8991
  let ai_avatar_response;
8904
8992
  try {
8905
8993
  ai_avatar_response = await client.images.edit({
8906
- model,
8994
+ model: resolve_ai_model(model),
8907
8995
  image: image_blob_ret.image_blob, //logo
8908
8996
  prompt: `
8909
8997
  Create a transparent PNG profile image for a business account featuring building branded as ${name}.
@@ -9528,7 +9616,7 @@ const create_ai_agent_image = async function (req, job_id, headers) {
9528
9616
  let ai_avatar_response;
9529
9617
  try {
9530
9618
  ai_avatar_response = await client.images.edit({
9531
- model,
9619
+ model: resolve_ai_model(model),
9532
9620
  image: image_blob_ret.image_blob,
9533
9621
 
9534
9622
  prompt,
@@ -15913,13 +16001,13 @@ const _ai_design = async (uid, src) => {
15913
16001
  `;
15914
16002
  let resp;
15915
16003
  try {
15916
- resp = await client.images.edit({ model, image: blob, prompt, background: 'transparent' });
16004
+ resp = await client.images.edit({ model: resolve_ai_model(model), image: blob, prompt, background: 'transparent' });
15917
16005
  report_ai_status(model);
15918
16006
  } catch (err) {
15919
16007
  report_ai_status(model, err);
15920
16008
  if (err?.code === 'moderation_blocked') {
15921
16009
  const soft = 'Create a futuristic metallic humanoid robot portrait with a transparent ' + 'background. Head and shoulders only, centered, forward-facing. Fully robotic and ' + 'non-photorealistic, glowing blue and purple neon accents, advanced reflective materials.';
15922
- resp = await client.images.edit({ model, image: blob, prompt: soft, background: 'transparent' });
16010
+ resp = await client.images.edit({ model: resolve_ai_model(model), image: blob, prompt: soft, background: 'transparent' });
15923
16011
  report_ai_status(model);
15924
16012
  } else {
15925
16013
  throw err;
@@ -16204,3 +16292,548 @@ export const widget_get_messages = async function (req, job_id, headers) {
16204
16292
  return { code: -1, data: err.message || String(err) };
16205
16293
  }
16206
16294
  };
16295
+
16296
+ ////////////////////////////// WIDGET SETTINGS (profile setup Chat tab) //////////////////////////////
16297
+
16298
+ // Public origin that serves the embed loaders, iframe routes and /cpi for
16299
+ // embedded widgets/forms. On dev the config domain still says xuda.ai, so a
16300
+ // dev-generated snippet must point at the dev host or the profile lookup 404s.
16301
+ const embed_origin = function () {
16302
+ if (_conf.is_debug && process.env.XUDA_HOSTNAME) return `https://${process.env.XUDA_HOSTNAME}`;
16303
+ return `https://${_conf.domain || 'xuda.ai'}`;
16304
+ };
16305
+
16306
+ const WIDGET_CONFIG_KEYS = ['title', 'color', 'greeting', 'position'];
16307
+ const WIDGET_POSITIONS = ['br', 'bl', 'tr', 'tl'];
16308
+
16309
+ const _sanitize_widget_config = function (config) {
16310
+ const out = {};
16311
+ if (!config || typeof config !== 'object') return out;
16312
+ if (typeof config.title === 'string') out.title = config.title.replace(/[<>]/g, '').slice(0, 60);
16313
+ if (typeof config.color === 'string' && /^#[0-9a-fA-F]{3,8}$/.test(config.color.trim())) out.color = config.color.trim();
16314
+ if (typeof config.greeting === 'string') out.greeting = config.greeting.replace(/[<>]/g, '').slice(0, 300);
16315
+ if (typeof config.position === 'string' && WIDGET_POSITIONS.includes(config.position)) out.position = config.position;
16316
+ return out;
16317
+ };
16318
+
16319
+ const _build_widget_snippet = function (widget_profile_id, config = {}) {
16320
+ const origin = embed_origin();
16321
+ const attrs = [`s.setAttribute('data-profile','${widget_profile_id}');`];
16322
+ if (config.position && WIDGET_POSITIONS.includes(config.position)) attrs.push(`s.setAttribute('data-position','${config.position}');`);
16323
+ if (config.color && /^#[0-9a-fA-F]{3,8}$/.test(config.color)) attrs.push(`s.setAttribute('data-color','${config.color}');`);
16324
+ return `<script>(function(){var d=document,s=d.createElement('script');s.src='${origin}/dist/runtime/js/chat-widget-loader.js';s.async=1;${attrs.join('')}d.head.appendChild(s);})();</script>`;
16325
+ };
16326
+
16327
+ // Read-only view of the profile's chat widget setup for the settings tab.
16328
+ // Unlike get_widget_embed_snippet this never mutates the profile doc.
16329
+ export const get_widget_settings = async function (req) {
16330
+ try {
16331
+ const { uid, profile_id } = req;
16332
+ const account_profile_info = await get_active_account_profile_info(uid, profile_id);
16333
+ if (account_profile_info.uid !== uid) return { code: -403, data: 'not_profile_owner' };
16334
+ const doc = account_profile_info.account_profile_obj || {};
16335
+ const widget_profile_id = `${account_profile_info.uid}.${account_profile_info.account_profile_id}`;
16336
+ const config = _sanitize_widget_config(doc.widget_config);
16337
+ return {
16338
+ code: 1,
16339
+ data: {
16340
+ enabled: doc.widget_enabled !== false,
16341
+ config,
16342
+ widget_profile_id,
16343
+ iframe_url: `${embed_origin()}/chat-widget/${widget_profile_id}`,
16344
+ snippet: _build_widget_snippet(widget_profile_id, config),
16345
+ },
16346
+ };
16347
+ } catch (err) {
16348
+ return { code: -1, data: err.message || String(err) };
16349
+ }
16350
+ };
16351
+
16352
+ // Save the Chat tab: on/off plus skin (title, color, position) and greeting.
16353
+ export const update_widget_settings = async function (req) {
16354
+ try {
16355
+ const { uid, profile_id, enabled, config } = req;
16356
+ const account_profile_info = await get_active_account_profile_info(uid, profile_id);
16357
+ if (account_profile_info.uid !== uid) return { code: -403, data: 'not_profile_owner' };
16358
+
16359
+ const doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, account_profile_info.account_profile_id);
16360
+ if (doc.docType !== 'account_profile') return { code: -1, data: 'not_account_profile' };
16361
+
16362
+ if (typeof enabled === 'boolean') doc.widget_enabled = enabled;
16363
+ if (config && typeof config === 'object') {
16364
+ const clean = _sanitize_widget_config(config);
16365
+ doc.widget_config = { ...(doc.widget_config || {}), ...clean };
16366
+ // Explicit empty string clears a key back to the default.
16367
+ for (const key of WIDGET_CONFIG_KEYS) {
16368
+ if (config[key] === '') delete doc.widget_config[key];
16369
+ }
16370
+ }
16371
+ doc.ts = Date.now();
16372
+ await db_module.save_app_couch_doc_native(account_profile_info.app_id, doc);
16373
+
16374
+ return await get_widget_settings({ uid, profile_id });
16375
+ } catch (err) {
16376
+ return { code: -1, data: err.message || String(err) };
16377
+ }
16378
+ };
16379
+
16380
+ ////////////////////////////// CONTACT FORM (embed) //////////////////////////////
16381
+
16382
+ // The form is a sibling of the chat widget: an iframe rendered by the router at
16383
+ // /contact-form/<uid>.<pid>, a loader script customers paste on their site, and
16384
+ // a public submit method. A submission adds the sender as a CONTACT on the
16385
+ // profile owner's side (no contact_connection team_request, so NOT a friend)
16386
+ // and opens a 'ticket' chat_conversation holding the message.
16387
+
16388
+ const CONTACT_FORM_TOKEN_TTL_MS = 2 * 60 * 60 * 1000;
16389
+ const CONTACT_FORM_MIN_AGE_MS = 3000;
16390
+ const CONTACT_FORM_FIELD_KEYS = ['phone', 'company', 'subject'];
16391
+
16392
+ // Human-detection token: HMAC over profile id + mint time. The key derives from
16393
+ // a fleet-shared secret so the router-rendered form and this module verify the
16394
+ // same signature without distributing a new secret.
16395
+ const _contact_form_hmac_key = function () {
16396
+ const seed = String(_conf?.gmail?.clientSecret || _conf?.domain || 'xuda');
16397
+ return crypto.createHash('sha256').update(`xuda_contact_form_v1:${seed}`).digest();
16398
+ };
16399
+
16400
+ export const mint_contact_form_token = function (widget_profile_id, ts = Date.now()) {
16401
+ const sig = crypto.createHmac('sha256', _contact_form_hmac_key()).update(`${widget_profile_id}.${ts}`).digest('hex').slice(0, 32);
16402
+ return `cft1.${ts}.${sig}`;
16403
+ };
16404
+
16405
+ const _verify_contact_form_token = function (widget_profile_id, token) {
16406
+ const parts = String(token || '').split('.');
16407
+ if (parts.length !== 3 || parts[0] !== 'cft1') return { ok: false, reason: 'bad_token' };
16408
+ const ts = Number(parts[1]);
16409
+ if (!Number.isFinite(ts)) return { ok: false, reason: 'bad_token' };
16410
+ const expected = crypto.createHmac('sha256', _contact_form_hmac_key()).update(`${widget_profile_id}.${ts}`).digest('hex').slice(0, 32);
16411
+ let sig_ok = false;
16412
+ try {
16413
+ sig_ok = crypto.timingSafeEqual(Buffer.from(parts[2]), Buffer.from(expected));
16414
+ } catch (err) {
16415
+ sig_ok = false;
16416
+ }
16417
+ if (!sig_ok) return { ok: false, reason: 'bad_token' };
16418
+ const age = Date.now() - ts;
16419
+ if (age > CONTACT_FORM_TOKEN_TTL_MS) return { ok: false, reason: 'token_expired' };
16420
+ if (age < CONTACT_FORM_MIN_AGE_MS) return { ok: false, reason: 'too_fast' };
16421
+ return { ok: true };
16422
+ };
16423
+
16424
+ const _sanitize_contact_form_config = function (config) {
16425
+ const out = {};
16426
+ if (!config || typeof config !== 'object') return out;
16427
+ const text = (v, max) => String(v).replace(/[<>]/g, '').slice(0, max);
16428
+ if (typeof config.title === 'string') out.title = text(config.title, 60);
16429
+ if (typeof config.intro === 'string') out.intro = text(config.intro, 300);
16430
+ if (typeof config.button_text === 'string') out.button_text = text(config.button_text, 40);
16431
+ if (typeof config.success_message === 'string') out.success_message = text(config.success_message, 300);
16432
+ if (typeof config.color === 'string' && /^#[0-9a-fA-F]{3,8}$/.test(config.color.trim())) out.color = config.color.trim();
16433
+ if (config.fields && typeof config.fields === 'object') {
16434
+ out.fields = {};
16435
+ for (const key of CONTACT_FORM_FIELD_KEYS) {
16436
+ const f = config.fields[key];
16437
+ if (f && typeof f === 'object') out.fields[key] = { show: f.show === true, required: f.required === true };
16438
+ }
16439
+ }
16440
+ if (typeof config.notify_email === 'boolean') out.notify_email = config.notify_email;
16441
+ if (typeof config.auto_respond === 'boolean') out.auto_respond = config.auto_respond;
16442
+ return out;
16443
+ };
16444
+
16445
+ const _build_contact_form_snippet = function (widget_profile_id, config = {}) {
16446
+ const origin = embed_origin();
16447
+ const attrs = [`s.setAttribute('data-profile','${widget_profile_id}');`];
16448
+ if (config.color && /^#[0-9a-fA-F]{3,8}$/.test(config.color)) attrs.push(`s.setAttribute('data-color','${config.color}');`);
16449
+ return `<script>(function(){var d=document,s=d.createElement('script');s.src='${origin}/dist/runtime/js/contact-form-loader.js';s.async=1;${attrs.join('')}d.head.appendChild(s);})();</script>`;
16450
+ };
16451
+
16452
+ export const get_contact_form_settings = async function (req) {
16453
+ try {
16454
+ const { uid, profile_id } = req;
16455
+ const account_profile_info = await get_active_account_profile_info(uid, profile_id);
16456
+ if (account_profile_info.uid !== uid) return { code: -403, data: 'not_profile_owner' };
16457
+ const doc = account_profile_info.account_profile_obj || {};
16458
+ const widget_profile_id = `${account_profile_info.uid}.${account_profile_info.account_profile_id}`;
16459
+ const config = _sanitize_contact_form_config(doc.contact_form_config);
16460
+ return {
16461
+ code: 1,
16462
+ data: {
16463
+ enabled: doc.contact_form_enabled === true,
16464
+ config,
16465
+ widget_profile_id,
16466
+ iframe_url: `${embed_origin()}/contact-form/${widget_profile_id}`,
16467
+ snippet: _build_contact_form_snippet(widget_profile_id, config),
16468
+ // Inline mount hint for the settings tab to display next to the snippet.
16469
+ container_snippet: '<div data-xuda-contact-form></div>',
16470
+ },
16471
+ };
16472
+ } catch (err) {
16473
+ return { code: -1, data: err.message || String(err) };
16474
+ }
16475
+ };
16476
+
16477
+ export const update_contact_form_settings = async function (req) {
16478
+ try {
16479
+ const { uid, profile_id, enabled, config } = req;
16480
+ const account_profile_info = await get_active_account_profile_info(uid, profile_id);
16481
+ if (account_profile_info.uid !== uid) return { code: -403, data: 'not_profile_owner' };
16482
+
16483
+ const doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, account_profile_info.account_profile_id);
16484
+ if (doc.docType !== 'account_profile') return { code: -1, data: 'not_account_profile' };
16485
+
16486
+ if (typeof enabled === 'boolean') doc.contact_form_enabled = enabled;
16487
+ if (config && typeof config === 'object') {
16488
+ const clean = _sanitize_contact_form_config(config);
16489
+ doc.contact_form_config = { ...(doc.contact_form_config || {}), ...clean };
16490
+ for (const key of ['title', 'intro', 'button_text', 'success_message', 'color']) {
16491
+ if (config[key] === '') delete doc.contact_form_config[key];
16492
+ }
16493
+ }
16494
+ doc.ts = Date.now();
16495
+ await db_module.save_app_couch_doc_native(account_profile_info.app_id, doc);
16496
+
16497
+ return await get_contact_form_settings({ uid, profile_id });
16498
+ } catch (err) {
16499
+ return { code: -1, data: err.message || String(err) };
16500
+ }
16501
+ };
16502
+
16503
+ // Router-side bootstrap for rendering the form iframe: resolves the profile,
16504
+ // enforces the enabled gate and returns display config plus a fresh token.
16505
+ // Broker-only (not in cpi_methods); the router calls it via ai_ms.
16506
+ export const get_contact_form_bootstrap = async function (req) {
16507
+ try {
16508
+ const { widget_profile_id } = req;
16509
+ const [owner_uid, pid] = String(widget_profile_id || '').split('.');
16510
+ if (!owner_uid || !pid) return { code: -404, data: 'invalid_profile' };
16511
+
16512
+ const account_profile_info = await get_active_account_profile_info(owner_uid, pid);
16513
+ const doc = account_profile_info.account_profile_obj || {};
16514
+ if (doc.contact_form_enabled !== true) return { code: -404, data: 'contact_form_disabled' };
16515
+
16516
+ let avatar_url = '';
16517
+ let owner_name = '';
16518
+ try {
16519
+ const acct_name_ret = await account_ms.get_account_name({ uid_query: account_profile_info.uid });
16520
+ avatar_url = acct_name_ret?.data?.profile_picture || acct_name_ret?.data?.profile_avatar || doc.profile_picture || doc.profile_avatar || '';
16521
+ owner_name = acct_name_ret?.data?.full_name || '';
16522
+ } catch (err) {}
16523
+
16524
+ const canonical_id = `${account_profile_info.uid}.${account_profile_info.account_profile_id}`;
16525
+ return {
16526
+ code: 1,
16527
+ data: {
16528
+ widget_profile_id: canonical_id,
16529
+ config: _sanitize_contact_form_config(doc.contact_form_config),
16530
+ profile_name: doc.profile_name || owner_name || '',
16531
+ avatar_url,
16532
+ token: mint_contact_form_token(canonical_id),
16533
+ },
16534
+ };
16535
+ } catch (err) {
16536
+ return { code: -404, data: err.message || String(err) };
16537
+ }
16538
+ };
16539
+
16540
+ // In-memory submit throttles. Per-process is fine: the goal is stopping dumb
16541
+ // bot floods, not building an accounting-grade limiter.
16542
+ const _contact_form_hits = { ip: {}, profile: {} };
16543
+ const _contact_form_allow = function (bucket, key, max, window_ms) {
16544
+ const now = Date.now();
16545
+ const store = _contact_form_hits[bucket];
16546
+ store[key] = (store[key] || []).filter((t) => now - t < window_ms);
16547
+ if (store[key].length >= max) return false;
16548
+ store[key].push(now);
16549
+ if (Object.keys(store).length > 5000) {
16550
+ for (const k of Object.keys(store)) {
16551
+ if (!store[k].length || now - store[k][store[k].length - 1] > window_ms) delete store[k];
16552
+ }
16553
+ }
16554
+ return true;
16555
+ };
16556
+
16557
+ // Open a 'ticket' chat_conversation on the owner's app for a form submission.
16558
+ // Every submission opens a fresh ticket (real ticket semantics), carrying the
16559
+ // message as the first inbound conversation item.
16560
+ const _contact_form_create_ticket = async function (account_profile_info, contact_id, subject, message, origin) {
16561
+ const owner_uid = account_profile_info.uid;
16562
+ const profile_id = account_profile_info.account_profile_id;
16563
+
16564
+ let conversation_obj = null;
16565
+ try {
16566
+ conversation_obj = await create_openai_conversation();
16567
+ } catch (err) {}
16568
+
16569
+ const d = Date.now();
16570
+ const conversation_doc = {
16571
+ _id: await _common.xuda_get_uuid('chat_conversation'),
16572
+ docType: 'chat_conversation',
16573
+ title: subject || getFirstNWords(message, 10),
16574
+ subject: subject || '',
16575
+ date_created_ts: d,
16576
+ ts: d,
16577
+ stat: 3,
16578
+ uid: owner_uid,
16579
+ messages: [],
16580
+ reference_type: 'contacts',
16581
+ reference_id: contact_id,
16582
+ conversation_obj,
16583
+ conversation_type: 'ticket',
16584
+ conversation_source: 'contact_form',
16585
+ contact_form_origin: origin || '',
16586
+ initiator_uid: owner_uid,
16587
+ direction: 'in',
16588
+ reference_conversation_id: conversation_obj?.id,
16589
+ account_profiles: [profile_id],
16590
+ prompt: message,
16591
+ rtl: _common.detectRTL(message),
16592
+ process_stat: 'full',
16593
+ };
16594
+ const conv_save = await db_module.save_app_couch_doc(account_profile_info.app_id, conversation_doc);
16595
+ if (conv_save.code < 0) throw new Error(conv_save.data || 'ticket_create_failed');
16596
+
16597
+ let conversation_item_reference_id;
16598
+ try {
16599
+ conversation_item_reference_id = await add_conversation_item(owner_uid, profile_id, conversation_doc._id, message, 'ticket', contact_id, { direction: 'in' });
16600
+ } catch (err) {}
16601
+
16602
+ const item_doc = {
16603
+ _id: await _common.xuda_get_uuid('chat_conversation_item'),
16604
+ stat: 3,
16605
+ docType: 'chat_conversation_item',
16606
+ uid: owner_uid,
16607
+ conversation_type: 'ticket',
16608
+ type: 'ticket',
16609
+ date_created_ts: d,
16610
+ ts: d,
16611
+ conversation_id: conversation_doc._id,
16612
+ text: message,
16613
+ subject: subject || '',
16614
+ reference_id: contact_id,
16615
+ conversation_item_reference_id,
16616
+ direction: 'in',
16617
+ role: 'user',
16618
+ contact_form_source: true,
16619
+ rtl: _common.detectRTL(message),
16620
+ };
16621
+ await db_module.save_app_couch_doc(account_profile_info.app_id, item_doc);
16622
+
16623
+ return conversation_doc;
16624
+ };
16625
+
16626
+ // Public endpoint behind the embedded form. Validates the human-detection
16627
+ // signals, adds the sender as a contact (not a friend) and opens the ticket.
16628
+ export const contact_form_submit = async function (req, job_id, headers) {
16629
+ try {
16630
+ const { profile_id, token, website, origin, visitor_lang } = req;
16631
+ let { name, email, subject, phone, company, message } = req;
16632
+
16633
+ // Honeypot: the hidden 'website' input must stay empty. Bots fill it;
16634
+ // answer with a generic ok so they learn nothing.
16635
+ if (typeof website === 'string' && website.trim() !== '') return { code: 1, data: { ok: true } };
16636
+
16637
+ if (typeof name !== 'string' || typeof email !== 'string' || typeof message !== 'string') return { code: -1, data: 'invalid_input' };
16638
+ name = name.trim().slice(0, 120);
16639
+ email = email.toLowerCase().trim();
16640
+ message = message.trim().slice(0, 5000);
16641
+ subject = typeof subject === 'string' ? subject.trim().slice(0, 200) : '';
16642
+ phone = typeof phone === 'string' ? phone.trim().slice(0, 40) : '';
16643
+ company = typeof company === 'string' ? company.trim().slice(0, 120) : '';
16644
+ if (!name || !email || !message) return { code: -1, data: 'invalid_input' };
16645
+ if (!WIDGET_EMAIL_RE.test(email)) return { code: -1, data: 'invalid_email' };
16646
+
16647
+ const [owner_uid_part, pid_part] = String(profile_id || '').split('.');
16648
+ if (!owner_uid_part || !pid_part) return { code: -404, data: 'invalid_profile' };
16649
+
16650
+ const account_profile_info = await get_active_account_profile_info(owner_uid_part, pid_part);
16651
+ const ap_doc = account_profile_info.account_profile_obj || {};
16652
+ if (ap_doc.contact_form_enabled !== true) return { code: -404, data: 'contact_form_disabled' };
16653
+
16654
+ const canonical_id = `${account_profile_info.uid}.${account_profile_info.account_profile_id}`;
16655
+ const token_check = _verify_contact_form_token(canonical_id, token);
16656
+ if (!token_check.ok) return { code: -401, data: token_check.reason };
16657
+
16658
+ const client_info = extractClientInfo(headers) || {};
16659
+ const client_ip = client_info.ipAddress || 'unknown';
16660
+ if (!_contact_form_allow('ip', client_ip, 5, 10 * 60 * 1000)) return { code: -429, data: 'rate_limited' };
16661
+ if (!_contact_form_allow('profile', canonical_id, 200, 24 * 60 * 60 * 1000)) return { code: -429, data: 'rate_limited' };
16662
+
16663
+ const owner_uid = account_profile_info.uid;
16664
+ const config = _sanitize_contact_form_config(ap_doc.contact_form_config);
16665
+
16666
+ // Field requirements set by the owner are enforced server side too.
16667
+ for (const key of CONTACT_FORM_FIELD_KEYS) {
16668
+ if (config.fields?.[key]?.show && config.fields?.[key]?.required) {
16669
+ const val = key === 'phone' ? phone : key === 'company' ? company : subject;
16670
+ if (!val) return { code: -1, data: `missing_${key}` };
16671
+ }
16672
+ }
16673
+
16674
+ // All human-detection gates passed: ack the visitor immediately and run
16675
+ // the pipeline (contact enrichment, ticket, notification, auto-reply) in
16676
+ // the background. add_contact runs several AI enrichment steps and would
16677
+ // otherwise hold the visitor's spinner for many seconds.
16678
+ (async () => {
16679
+ try {
16680
+ // Contact, not friend: no contact_connection team_request is created.
16681
+ const add_ret = await account_ms.add_contact({
16682
+ uid: owner_uid,
16683
+ profile_id: account_profile_info.account_profile_id,
16684
+ name,
16685
+ email,
16686
+ source: 'contact_form',
16687
+ context: `Contact form submission${origin ? ` from ${String(origin).slice(0, 200)}` : ''}`,
16688
+ metadata: {
16689
+ subject,
16690
+ phone,
16691
+ company,
16692
+ origin: String(origin || '').slice(0, 200),
16693
+ visitor_lang: String(visitor_lang || '').slice(0, 16),
16694
+ contact_form: true,
16695
+ not_junk: true,
16696
+ },
16697
+ });
16698
+ const contact_id = add_ret.code > -1 ? add_ret.data?.id : add_ret.contact_id;
16699
+ if (!contact_id) throw new Error(add_ret.data || 'contact_create_failed');
16700
+
16701
+ const body_parts = [message];
16702
+ const detail_bits = [phone && `Phone: ${phone}`, company && `Company: ${company}`].filter(Boolean);
16703
+ if (detail_bits.length) body_parts.push(detail_bits.join(' | '));
16704
+ await _contact_form_create_ticket(account_profile_info, contact_id, subject, body_parts.join('\n\n'), origin);
16705
+
16706
+ // Owner notification email (platform template).
16707
+ if (config.notify_email !== false) {
16708
+ try {
16709
+ const owner_acct = await db_module.get_couch_doc('xuda_accounts', owner_uid);
16710
+ const owner_email = owner_acct?.data?.account_info?.email || owner_acct?.data?.email;
16711
+ if (owner_email) {
16712
+ const esc = (v) => String(v || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
16713
+ const dashboard_url = `${embed_origin()}/dashboard`;
16714
+ const body = `<h2 style="margin:0 0 12px">New contact form message</h2>
16715
+ <p style="margin:0 0 16px;color:#475569">Someone reached out through the contact form of your profile <b>${esc(ap_doc.profile_name || '')}</b>.</p>
16716
+ <table cellpadding="0" cellspacing="0" style="width:100%;border-collapse:collapse;font-size:14px">
16717
+ <tr><td style="padding:6px 12px 6px 0;color:#64748b;white-space:nowrap;vertical-align:top">From</td><td style="padding:6px 0"><b>${esc(name)}</b> &lt;${esc(email)}&gt;</td></tr>
16718
+ ${subject ? `<tr><td style="padding:6px 12px 6px 0;color:#64748b;vertical-align:top">Subject</td><td style="padding:6px 0">${esc(subject)}</td></tr>` : ''}
16719
+ ${phone ? `<tr><td style="padding:6px 12px 6px 0;color:#64748b;vertical-align:top">Phone</td><td style="padding:6px 0">${esc(phone)}</td></tr>` : ''}
16720
+ ${company ? `<tr><td style="padding:6px 12px 6px 0;color:#64748b;vertical-align:top">Company</td><td style="padding:6px 0">${esc(company)}</td></tr>` : ''}
16721
+ </table>
16722
+ <div style="background:#f8fafc;border:1px solid #e6ebf1;border-radius:10px;padding:14px 16px;margin:14px 0;white-space:pre-wrap">${esc(message)}</div>
16723
+ <p style="margin:16px 0 0"><a href="${dashboard_url}" style="display:inline-block;background:#0091FF;color:#fff;text-decoration:none;padding:10px 22px;border-radius:8px;font-weight:600">Open the conversation</a></p>`;
16724
+ await email_ms.send_email({ email: owner_email, subject: `New contact form message from ${name}`, body, display_type: 'info' });
16725
+ }
16726
+ } catch (err) {
16727
+ console.error('[contact_form] owner notification failed', err?.message || err);
16728
+ }
16729
+ }
16730
+
16731
+ // Optional AI auto-reply, reusing the profile's auto-respond setup.
16732
+ if (config.auto_respond !== false && ap_doc.auto_respond === true) {
16733
+ auto_response(owner_uid, account_profile_info.account_profile_id, contact_id, 'ticket');
16734
+ }
16735
+ } catch (err) {
16736
+ console.error('[contact_form_submit] background pipeline failed', err?.message || err);
16737
+ }
16738
+ })();
16739
+
16740
+ return { code: 1, data: { ok: true } };
16741
+ } catch (err) {
16742
+ return { code: -1, data: err.message || String(err) };
16743
+ }
16744
+ };
16745
+
16746
+ // Owner-side reply to a 'ticket' conversation (submit_chat_conversation case).
16747
+ // Appends the outbound item and emails the contact: through the profile's
16748
+ // connected mailbox when one exists, otherwise through the platform mail
16749
+ // server with Reply-To pointing at the owner. The body is wrapped with the
16750
+ // profile's selected email template (Email Template tab) unless it is plain.
16751
+ const contact_ticket_conversation = async function (req, job_id, headers) {
16752
+ const { uid, profile_id, _auto_response = false } = req;
16753
+ const account_profile_info = await get_active_account_profile_info(uid, profile_id);
16754
+ let { prompt: body, conversation_doc } = req;
16755
+ try {
16756
+ const conversation_id = conversation_doc._id;
16757
+ const sender_app_id = account_profile_info.app_id;
16758
+ const contact_id = conversation_doc.reference_id;
16759
+ const contact_info = await get_contact_info(uid, null, contact_id);
16760
+ if (!contact_info?.email) throw new Error('empty contact email');
16761
+
16762
+ const subject_base = conversation_doc.subject || conversation_doc.title;
16763
+ const subject = subject_base ? `Re: ${subject_base}` : 'Re: your message';
16764
+
16765
+ const profile_doc = account_profile_info.account_profile_obj || {};
16766
+ let html = null;
16767
+ try {
16768
+ const render_ret = await email_ms.render_profile_email({
16769
+ style: profile_doc.email_template?.style,
16770
+ body_text: body,
16771
+ profile_name: profile_doc.profile_name,
16772
+ signature: profile_doc.profile_signature,
16773
+ avatar_url: profile_doc.profile_picture || profile_doc.profile_avatar || '',
16774
+ brand_color: profile_doc.widget_config?.color || profile_doc.contact_form_config?.color || '',
16775
+ });
16776
+ if (render_ret?.code > 0 && render_ret.data) html = render_ret.data;
16777
+ } catch (err) {}
16778
+
16779
+ let sent = false;
16780
+ if (profile_doc.email_account_id) {
16781
+ try {
16782
+ const email_account_doc = await db_module.get_app_couch_doc_native(sender_app_id, profile_doc.email_account_id);
16783
+ const send_ret = await email_ms.sendEmailFromAccount(email_account_doc, contact_info.email, subject, body, html);
16784
+ sent = !!send_ret?.success;
16785
+ } catch (err) {
16786
+ console.error('[ticket reply] profile mailbox send failed, falling back to platform mail', err?.message || err);
16787
+ }
16788
+ }
16789
+ if (!sent) {
16790
+ const owner_acct = await db_module.get_couch_doc('xuda_accounts', uid);
16791
+ const owner_email = owner_acct?.data?.account_info?.email || owner_acct?.data?.email || '';
16792
+ const send_ret = await email_ms.send_profile_email({
16793
+ to: contact_info.email,
16794
+ subject,
16795
+ body_text: body,
16796
+ html,
16797
+ from_name: profile_doc.profile_name || owner_acct?.data?.account_info?.full_name || 'Xuda user',
16798
+ reply_to: owner_email,
16799
+ });
16800
+ sent = !!send_ret?.success || send_ret?.code > 0;
16801
+ if (!sent) throw new Error('error sending ticket reply email');
16802
+ }
16803
+
16804
+ let conversation_item_reference_id;
16805
+ try {
16806
+ conversation_item_reference_id = await add_conversation_item(uid, profile_id, conversation_id, body, 'ticket', contact_id, { direction: 'out' });
16807
+ } catch (err) {}
16808
+
16809
+ const now = Date.now();
16810
+ const out_conversation_item_obj = {
16811
+ _id: await _common.xuda_get_uuid('chat_conversation_item'),
16812
+ stat: 3,
16813
+ docType: 'chat_conversation_item',
16814
+ uid,
16815
+ conversation_type: 'ticket',
16816
+ type: 'ticket',
16817
+ date_created_ts: now,
16818
+ ts: now,
16819
+ conversation_id,
16820
+ text: body,
16821
+ subject,
16822
+ reference_id: contact_id,
16823
+ conversation_item_reference_id,
16824
+ direction: 'out',
16825
+ role: 'user',
16826
+ auto_response: _auto_response || undefined,
16827
+ rtl: _common.detectRTL(body),
16828
+ };
16829
+ const save_ret = await db_module.save_app_couch_doc(sender_app_id, out_conversation_item_obj);
16830
+
16831
+ const fresh_conv = await db_module.get_app_couch_doc_native(sender_app_id, conversation_id);
16832
+ fresh_conv.ts = Date.now();
16833
+ await db_module.save_app_couch_doc_native(sender_app_id, fresh_conv);
16834
+
16835
+ return save_ret;
16836
+ } catch (err) {
16837
+ return { code: -15, data: err.message };
16838
+ }
16839
+ };
package/index_ms.mjs CHANGED
@@ -320,3 +320,31 @@ export const widget_send_message = async function (...args) {
320
320
  export const widget_get_messages = async function (...args) {
321
321
  return await broker.send_to_queue("widget_get_messages", ...args);
322
322
  };
323
+
324
+ export const get_widget_settings = async function (...args) {
325
+ return await broker.send_to_queue("get_widget_settings", ...args);
326
+ };
327
+
328
+ export const update_widget_settings = async function (...args) {
329
+ return await broker.send_to_queue("update_widget_settings", ...args);
330
+ };
331
+
332
+ export const mint_contact_form_token = async function (...args) {
333
+ return await broker.send_to_queue("mint_contact_form_token", ...args);
334
+ };
335
+
336
+ export const get_contact_form_settings = async function (...args) {
337
+ return await broker.send_to_queue("get_contact_form_settings", ...args);
338
+ };
339
+
340
+ export const update_contact_form_settings = async function (...args) {
341
+ return await broker.send_to_queue("update_contact_form_settings", ...args);
342
+ };
343
+
344
+ export const get_contact_form_bootstrap = async function (...args) {
345
+ return await broker.send_to_queue("get_contact_form_bootstrap", ...args);
346
+ };
347
+
348
+ export const contact_form_submit = async function (...args) {
349
+ return await broker.send_to_queue("contact_form_submit", ...args);
350
+ };
package/index_msa.mjs CHANGED
@@ -320,3 +320,31 @@ export const widget_send_message = function (...args) {
320
320
  export const widget_get_messages = function (...args) {
321
321
  broker.send_to_queue_async("widget_get_messages", ...args);
322
322
  };
323
+
324
+ export const get_widget_settings = function (...args) {
325
+ broker.send_to_queue_async("get_widget_settings", ...args);
326
+ };
327
+
328
+ export const update_widget_settings = function (...args) {
329
+ broker.send_to_queue_async("update_widget_settings", ...args);
330
+ };
331
+
332
+ export const mint_contact_form_token = function (...args) {
333
+ broker.send_to_queue_async("mint_contact_form_token", ...args);
334
+ };
335
+
336
+ export const get_contact_form_settings = function (...args) {
337
+ broker.send_to_queue_async("get_contact_form_settings", ...args);
338
+ };
339
+
340
+ export const update_contact_form_settings = function (...args) {
341
+ broker.send_to_queue_async("update_contact_form_settings", ...args);
342
+ };
343
+
344
+ export const get_contact_form_bootstrap = function (...args) {
345
+ broker.send_to_queue_async("get_contact_form_bootstrap", ...args);
346
+ };
347
+
348
+ export const contact_form_submit = function (...args) {
349
+ broker.send_to_queue_async("contact_form_submit", ...args);
350
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.5634",
3
+ "version": "1.1.5635",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",