@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 +170 -0
- package/index_ms.mjs +12 -0
- package/index_msa.mjs +12 -0
- package/package.json +5 -2
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.
|
|
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
|
}
|