@xuda.io/ai_module 1.1.5632 → 1.1.5634

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
@@ -8638,6 +8638,63 @@ export const create_avatar = async function (req, job_id, headers) {
8638
8638
  }
8639
8639
  };
8640
8640
 
8641
+ // generate_login_screen: paid-plan alternative to uploading — AI-generate a
8642
+ // full-bleed landscape login background from a text prompt. Credit-gated like
8643
+ // avatar generation (create_image records usage). The PNG render is re-encoded to
8644
+ // WebP to satisfy the 5 MB spec, then handed to the shared drive writer over the
8645
+ // drive_module queue so it is persisted + served publicly exactly like an upload.
8646
+ export const generate_login_screen = async function (req, job_id, headers = {}) {
8647
+ const { uid, prompt } = req;
8648
+ const tempDir = os.tmpdir();
8649
+ const uniqueId = Date.now() + '_' + Math.random().toString(36).substring(7);
8650
+ const tempOutputPath = path.join(tempDir, `login_screen_${uniqueId}.webp`);
8651
+ try {
8652
+ if (!uid) throw new Error('not authorized (no uid)');
8653
+ if (!prompt || typeof prompt !== 'string' || !prompt.trim()) throw new Error('a prompt is required');
8654
+
8655
+ const account_profile_info = await get_active_account_profile_info(uid);
8656
+
8657
+ // credits: validate_credits_limit RETURNS an Error object when over the hard limit
8658
+ const credit_err = await validate_credits_limit(uid);
8659
+ if (credit_err) return { code: -25, data: 'AI credits reached the hard limit' };
8660
+
8661
+ const wrapped_prompt = `Create a full-bleed, decorative LANDSCAPE background image for a login screen. Subject/style: ${prompt.trim()}. It must contain no text, letters, logos, watermarks, UI elements, or human faces; use a cohesive, calm color palette with soft contrast so a login form stays readable on top. Fill the whole frame edge to edge.`;
8662
+
8663
+ // gpt-image landscape render; quality 'low' is fine for a decorative background.
8664
+ const base64 = await create_image(uid, wrapped_prompt, 'gpt-image-1-mini', '1536x1024', 1, 1536, 1024, { purpose: 'login_screen' }, account_profile_info, 'low');
8665
+ if (!base64) throw new Error('image generation failed');
8666
+
8667
+ // create_image emits PNG; re-encode to WebP to stay well under the 5 MB spec.
8668
+ const webp = await sharp(Buffer.from(base64, 'base64')).webp({ quality: 82 }).toBuffer();
8669
+ await fs.promises.writeFile(tempOutputPath, webp);
8670
+
8671
+ const file_obj = { originalname: 'login_screen.webp', path: tempOutputPath, mimetype: 'image/webp' };
8672
+ const store_ret = await drive_ms.store_login_screen_from_path(uid, file_obj, job_id, headers);
8673
+ if (store_ret.code !== 1) return store_ret;
8674
+
8675
+ const o = store_ret.data.login_screen_obj;
8676
+ return {
8677
+ code: 1,
8678
+ data: {
8679
+ login_screen: {
8680
+ source: 'personal',
8681
+ media_type: 'image',
8682
+ file_url: o.file_url,
8683
+ w: o.w,
8684
+ h: o.h,
8685
+ bytes: o.bytes,
8686
+ uploaded_ts: o.uploaded_ts,
8687
+ },
8688
+ },
8689
+ };
8690
+ } catch (err) {
8691
+ try {
8692
+ await fs.promises.unlink(tempOutputPath);
8693
+ } catch (e) {}
8694
+ return { code: -25, data: err.message };
8695
+ }
8696
+ };
8697
+
8641
8698
  export const get_profile_avatar = async function (profile_picture, uid, prompt, account_profile_info, account_type, docType = 'account', _id, metadata = {}, business_size = 'unknown', name, email, job_id, headers) {
8642
8699
  const tempDir = os.tmpdir();
8643
8700
  const uniqueId = Date.now() + '_' + Math.random().toString(36).substring(7);
@@ -14937,13 +14994,26 @@ const _widget_verify_google_id_token = async function (id_token) {
14937
14994
  return payload;
14938
14995
  };
14939
14996
 
14997
+ // Widget/site visitors never hit the dashboard login, which is what normally
14998
+ // runs misc.login_maintenance_fix (plans, Stripe customer, starter credits).
14999
+ // Run it from here instead, once per account: gate on stripe_customer_id since
15000
+ // create_stripe_customer is the step that stamps membership_plan. Fire-and-forget
15001
+ // so signup latency is unaffected; without token_ret the account_project_id fix
15002
+ // throws by design AFTER the plan/credit fixes have completed.
15003
+ const _widget_account_maintenance = function (uid) {
15004
+ try {
15005
+ misc_msa.login_maintenance_fix(uid, null);
15006
+ } catch (err) {}
15007
+ };
15008
+
14940
15009
  const _widget_find_or_create_visitor = async function (email, name, google_sub, picture) {
14941
15010
  const ret = await db_module.find_couch_query('xuda_accounts', {
14942
15011
  selector: { 'account_info.email': email },
14943
- fields: ['_id'],
15012
+ fields: ['_id', 'stripe_customer_id'],
14944
15013
  limit: 1,
14945
15014
  });
14946
15015
  if (ret.docs && ret.docs.length) {
15016
+ if (!ret.docs[0].stripe_customer_id) _widget_account_maintenance(ret.docs[0]._id);
14947
15017
  // If we have a google_sub and the existing account lacks one, attach it.
14948
15018
  // Also seed the Google photo as the profile_picture when the account has no
14949
15019
  // avatar yet, so avatar generation has a source to work from.
@@ -14987,6 +15057,9 @@ const _widget_find_or_create_visitor = async function (email, name, google_sub,
14987
15057
  }
14988
15058
  const doc = {
14989
15059
  _id: await _common.xuda_get_uuid('account'),
15060
+ // 1 = unverified (Boaz 2026-07-06): widget signups stay plain unverified
15061
+ // until a real dashboard login; keeps them out of search_users (stat 3)
15062
+ // and out of the missing-plan report (stat-3-only).
14990
15063
  stat: 1,
14991
15064
  docType: 'account',
14992
15065
  source: 'widget',
@@ -15000,6 +15073,7 @@ const _widget_find_or_create_visitor = async function (email, name, google_sub,
15000
15073
  }
15001
15074
  const save_ret = await db_module.save_couch_doc('xuda_accounts', doc);
15002
15075
  if (save_ret.code < 0) throw new Error(save_ret.data || 'visitor_create_failed');
15076
+ _widget_account_maintenance(doc._id);
15003
15077
  return doc._id;
15004
15078
  };
15005
15079
 
package/index_ms.mjs CHANGED
@@ -157,6 +157,10 @@ export const create_avatar = async function (...args) {
157
157
  return await broker.send_to_queue("create_avatar", ...args);
158
158
  };
159
159
 
160
+ export const generate_login_screen = async function (...args) {
161
+ return await broker.send_to_queue("generate_login_screen", ...args);
162
+ };
163
+
160
164
  export const get_profile_avatar = async function (...args) {
161
165
  return await broker.send_to_queue("get_profile_avatar", ...args);
162
166
  };
package/index_msa.mjs CHANGED
@@ -157,6 +157,10 @@ export const create_avatar = function (...args) {
157
157
  broker.send_to_queue_async("create_avatar", ...args);
158
158
  };
159
159
 
160
+ export const generate_login_screen = function (...args) {
161
+ broker.send_to_queue_async("generate_login_screen", ...args);
162
+ };
163
+
160
164
  export const get_profile_avatar = function (...args) {
161
165
  broker.send_to_queue_async("get_profile_avatar", ...args);
162
166
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.5632",
3
+ "version": "1.1.5634",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",
@@ -31,5 +31,8 @@
31
31
  "test:codex": "node tests/execute-codex-request.mjs"
32
32
  },
33
33
  "author": "Xuda Llc",
34
- "license": "ISC"
34
+ "license": "ISC",
35
+ "publishConfig": {
36
+ "access": "restricted"
37
+ }
35
38
  }