@xuda.io/drive_module 1.1.1494 → 1.1.1495

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
@@ -1790,6 +1790,7 @@ export const upload_drive_file = async (req, job_id, headers = {}, file_obj) =>
1790
1790
  filename: file_obj.originalname,
1791
1791
  file_url,
1792
1792
  file_path,
1793
+ bucket: doc.bucket, // region->{Bucket,Key,Location}; undefined for scp/datacenter paths
1793
1794
  },
1794
1795
  rename: check_existing_file_ret < 0,
1795
1796
  };
@@ -1797,6 +1798,175 @@ export const upload_drive_file = async (req, job_id, headers = {}, file_obj) =>
1797
1798
  return { code: -400, data: err.message };
1798
1799
  }
1799
1800
  };
1801
+ // ============================================================================
1802
+ // Custom login-screen background (personal + sponsor image)
1803
+ // ============================================================================
1804
+ // A non-free user's login-screen background lives on the account doc at
1805
+ // account_info.login_screen_obj and is served pre-auth (see account_module
1806
+ // resolve_effective_login_screen). It is set either by uploading a picture
1807
+ // (set_login_screen) or by AI generation (ai_module.generate_login_screen); both
1808
+ // funnel their temp file through store_login_screen_from_path so there is exactly
1809
+ // one writer of login_screen_obj. SVG is rejected (pre-auth XSS vector) and the
1810
+ // magic-bytes are cross-checked against the extension to block image polyglots.
1811
+ export const LOGIN_SCREEN_SPEC = {
1812
+ formats: ['jpg', 'jpeg', 'png', 'webp'],
1813
+ max_bytes: 5 * 1024 * 1024,
1814
+ min_w: 1280,
1815
+ min_h: 720,
1816
+ max_w: 5000,
1817
+ max_h: 5000,
1818
+ aspect_min: 1.2,
1819
+ aspect_max: 2.1,
1820
+ };
1821
+
1822
+ // Validate a login-screen image on disk against LOGIN_SCREEN_SPEC. Always unlinks
1823
+ // the temp file on failure (mirrors upload_drive_file's cleanup); on success it
1824
+ // leaves the file for the uploader and returns the measured { w, h, bytes, mime, ext }.
1825
+ export const validate_login_screen_file = async (file_obj) => {
1826
+ const S = LOGIN_SCREEN_SPEC;
1827
+ const temp_path = file_obj?.path;
1828
+ const fail = async (error) => {
1829
+ try {
1830
+ if (temp_path) await fs_module.unlink(temp_path);
1831
+ } catch (e) {}
1832
+ return { valid: false, error };
1833
+ };
1834
+ if (!file_obj?.originalname || !temp_path) return await fail('file data missing');
1835
+
1836
+ const ext = path.extname(file_obj.originalname).replace('.', '').toLowerCase();
1837
+ if (!S.formats.includes(ext)) return await fail(`Only ${S.formats.join(', ')} images are allowed`);
1838
+
1839
+ const validation = file_upload_validator(file_obj.originalname);
1840
+ if (!validation.valid || validation.category !== 'image') return await fail('File type not allowed');
1841
+
1842
+ let bytes = 0;
1843
+ try {
1844
+ bytes = (await fs.promises.stat(temp_path)).size;
1845
+ } catch (e) {
1846
+ return await fail('cannot read uploaded file');
1847
+ }
1848
+ if (bytes > S.max_bytes) return await fail(`Image is too large (max ${Math.round(S.max_bytes / 1024 / 1024)} MB)`);
1849
+
1850
+ let dim;
1851
+ try {
1852
+ dim = imageSize(temp_path);
1853
+ } catch (e) {
1854
+ dim = null;
1855
+ }
1856
+ if (!dim?.width || !dim?.height) return await fail('could not read image dimensions');
1857
+
1858
+ // magic-byte type must itself be an accepted raster format — blocks a .png that
1859
+ // is actually an SVG/HTML polyglot, and svg smuggled under a jpg/webp name.
1860
+ const magic = String(dim.type || '').toLowerCase();
1861
+ if (!S.formats.includes(magic)) return await fail('image content does not match an allowed format');
1862
+
1863
+ const { width: w, height: h } = dim;
1864
+ if (w < S.min_w || h < S.min_h) return await fail(`Image must be at least ${S.min_w}x${S.min_h}px`);
1865
+ if (w > S.max_w || h > S.max_h) return await fail(`Image must be at most ${S.max_w}x${S.max_h}px`);
1866
+ const aspect = w / h;
1867
+ if (aspect < S.aspect_min || aspect > S.aspect_max) return await fail(`Image must be landscape (aspect ratio ${S.aspect_min}–${S.aspect_max})`);
1868
+
1869
+ return { valid: true, w, h, bytes, mime: validation.mime, ext };
1870
+ };
1871
+
1872
+ // Shared writer for a login-screen image: paid-plan gate -> validate -> persist
1873
+ // through the user-drive path avatars use (so file_url is served publicly by the
1874
+ // region; a direct R2 URL is NOT anonymously readable) -> free the previous asset
1875
+ // -> save account_info.login_screen_obj. Called with a local temp file (same box)
1876
+ // by set_login_screen and, over the drive_module queue, by ai_module.generate_login_screen.
1877
+ export const store_login_screen_from_path = async (uid, file_obj, job_id = null, headers = {}) => {
1878
+ const temp_path = file_obj?.path;
1879
+ const cleanup = async () => {
1880
+ try {
1881
+ if (temp_path) await fs_module.unlink(temp_path);
1882
+ } catch (e) {}
1883
+ };
1884
+ try {
1885
+ if (!uid) {
1886
+ await cleanup();
1887
+ return { code: -401, data: 'not authorized (no uid)' };
1888
+ }
1889
+
1890
+ const { code: acc_code, data: account_obj } = await db_module.get_couch_doc('xuda_accounts', uid);
1891
+ if (acc_code < 0 || !account_obj) {
1892
+ await cleanup();
1893
+ return { code: -404, data: 'account not found' };
1894
+ }
1895
+ // Paid-plan gate (defense-in-depth on top of the min_plan cpi gate).
1896
+ if ((account_obj.membership_plan || 'free') === 'free') {
1897
+ await cleanup();
1898
+ return { code: -403, data: 'A custom login screen requires a paid plan' };
1899
+ }
1900
+
1901
+ const validation = await validate_login_screen_file(file_obj); // unlinks temp on failure
1902
+ if (!validation.valid) return { code: -400, data: validation.error };
1903
+
1904
+ const up = await upload_drive_file_user({ uid, path: '/Login Screen' }, job_id, headers, file_obj);
1905
+ if (!up || up.code < 0 || !up.data?.file_url) {
1906
+ await cleanup();
1907
+ return { code: -500, data: up?.data || 'upload failed' };
1908
+ }
1909
+
1910
+ const login_screen_obj = {
1911
+ file_url: up.data.file_url,
1912
+ server_file_name: up.data.server_file_name,
1913
+ bucket: up.data.bucket || null,
1914
+ w: validation.w,
1915
+ h: validation.h,
1916
+ bytes: validation.bytes,
1917
+ mime: validation.mime,
1918
+ uploaded_ts: Date.now(),
1919
+ };
1920
+
1921
+ // Free the previous asset's bytes so replacing the screen never orphans R2 blobs.
1922
+ const prev = account_obj.account_info?.login_screen_obj;
1923
+ if (prev?.bucket) {
1924
+ try {
1925
+ await delete_file_from_bucket(prev.bucket);
1926
+ } catch (e) {}
1927
+ }
1928
+
1929
+ account_obj.account_info = account_obj.account_info || {};
1930
+ account_obj.account_info.login_screen_obj = login_screen_obj;
1931
+ account_obj.ts = Date.now();
1932
+ await db_module.save_couch_doc('xuda_accounts', account_obj);
1933
+
1934
+ return { code: 1, data: { login_screen_obj } };
1935
+ } catch (err) {
1936
+ await cleanup();
1937
+ return { code: -400, data: err.message };
1938
+ }
1939
+ };
1940
+
1941
+ // set_login_screen: authenticated multipart upload (upload:true cpi_method). The
1942
+ // router passes the parsed file as the 4th arg; uid comes from the token, never the
1943
+ // body (spoof-safe). Returns the effective personal screen for an instant client
1944
+ // update; the authoritative resolve (sponsored > personal > default) runs in
1945
+ // account_module.get_account_data on the next bootstrap.
1946
+ export const set_login_screen = async (req, job_id, headers = {}, file_obj) => {
1947
+ const uid = req.uid || req.token_ret?.data?.uid;
1948
+ if (!uid) return { code: -401, data: 'not authorized (no uid)' };
1949
+ if (!file_obj?.originalname) return { code: -400, data: 'file data missing' };
1950
+
1951
+ const ret = await store_login_screen_from_path(uid, file_obj, job_id, headers);
1952
+ if (ret.code !== 1) return ret;
1953
+ const o = ret.data.login_screen_obj;
1954
+ return {
1955
+ code: 1,
1956
+ data: {
1957
+ login_screen: {
1958
+ source: 'personal',
1959
+ media_type: 'image',
1960
+ file_url: o.file_url,
1961
+ w: o.w,
1962
+ h: o.h,
1963
+ bytes: o.bytes,
1964
+ uploaded_ts: o.uploaded_ts,
1965
+ },
1966
+ },
1967
+ };
1968
+ };
1969
+
1800
1970
  const check_single_drive_file = async (req, job_id, headers, file_obj) => {
1801
1971
  const { type = 'file', drive_type, app_id, uid, file_path, folder_name } = req;
1802
1972
 
package/index_ms.mjs CHANGED
@@ -97,6 +97,18 @@ export const upload_drive_file = async function (...args) {
97
97
  return await broker.send_to_queue("upload_drive_file", ...args);
98
98
  };
99
99
 
100
+ export const validate_login_screen_file = async function (...args) {
101
+ return await broker.send_to_queue("validate_login_screen_file", ...args);
102
+ };
103
+
104
+ export const store_login_screen_from_path = async function (...args) {
105
+ return await broker.send_to_queue("store_login_screen_from_path", ...args);
106
+ };
107
+
108
+ export const set_login_screen = async function (...args) {
109
+ return await broker.send_to_queue("set_login_screen", ...args);
110
+ };
111
+
100
112
  export const check_drive_files = async function (...args) {
101
113
  return await broker.send_to_queue("check_drive_files", ...args);
102
114
  };
package/index_msa.mjs CHANGED
@@ -97,6 +97,18 @@ export const upload_drive_file = function (...args) {
97
97
  broker.send_to_queue_async("upload_drive_file", ...args);
98
98
  };
99
99
 
100
+ export const validate_login_screen_file = function (...args) {
101
+ broker.send_to_queue_async("validate_login_screen_file", ...args);
102
+ };
103
+
104
+ export const store_login_screen_from_path = function (...args) {
105
+ broker.send_to_queue_async("store_login_screen_from_path", ...args);
106
+ };
107
+
108
+ export const set_login_screen = function (...args) {
109
+ broker.send_to_queue_async("set_login_screen", ...args);
110
+ };
111
+
100
112
  export const check_drive_files = function (...args) {
101
113
  broker.send_to_queue_async("check_drive_files", ...args);
102
114
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/drive_module",
3
- "version": "1.1.1494",
3
+ "version": "1.1.1495",
4
4
  "description": "Xuda Drive Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {
@@ -24,5 +24,8 @@
24
24
  "pub": "npm version patch --force && npm publish "
25
25
  },
26
26
  "author": "Xuda Llc",
27
- "license": "ISC"
27
+ "license": "ISC",
28
+ "publishConfig": {
29
+ "access": "restricted"
30
+ }
28
31
  }