From 1ae4a5ac0e53d7021c05696f8d56ff41b27cf853 Mon Sep 17 00:00:00 2001 From: ShareVB Date: Fri, 31 Oct 2025 23:13:19 +0100 Subject: [PATCH 1/3] chore(Build): ignore isolated-vm --- .npmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..424dbca321 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +ignore-dependencies=isolated-vm \ No newline at end of file From 1aef5984c4627fbac8fa9ab06508b4e7aff6b1cd Mon Sep 17 00:00:00 2001 From: lionel-rowe Date: Mon, 3 Nov 2025 00:19:59 +0800 Subject: [PATCH 2/3] Use async versions of bcrypt methods to avoid freezing browser tab --- .npmrc | 1 - locales/en.yml | 8 +- scripts/extract-tools-strings.mjs | 4 +- src/plugins/i18n.plugin.ts | 13 ++- src/tools/bcrypt/bcrypt.models.test.ts | 49 +++++++++ src/tools/bcrypt/bcrypt.models.ts | 90 +++++++++++++++ src/tools/bcrypt/bcrypt.vue | 146 ++++++++++++++++++++++--- 7 files changed, 286 insertions(+), 25 deletions(-) delete mode 100644 .npmrc create mode 100644 src/tools/bcrypt/bcrypt.models.test.ts create mode 100644 src/tools/bcrypt/bcrypt.models.ts diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 424dbca321..0000000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -ignore-dependencies=isolated-vm \ No newline at end of file diff --git a/locales/en.yml b/locales/en.yml index 58a9da6b71..5feac0747e 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -226,8 +226,14 @@ tools: placeholder-your-string-to-compare: Your string to compare... label-your-hash: "Your hash: " placeholder-your-hash-to-compare: Your hash to compare... - label-do-they-match: "Do they match ? " tag-copy-hash: Copy hash + hashed-string: Hashed string + comparison-result: Comparison result + matched: Matched + no-match: No match + timed-out-after-timeout-period: Timed out after {timeoutPeriod} + hashed-in-elapsed-period: Hashed in {elapsedPeriod} + compared-in-elapsed-period: Compared in {elapsedPeriod} crontab-generator: title: Crontab generator description: Validate and generate crontab and get the human-readable diff --git a/scripts/extract-tools-strings.mjs b/scripts/extract-tools-strings.mjs index 2766c915bb..a1fd618f3f 100644 --- a/scripts/extract-tools-strings.mjs +++ b/scripts/extract-tools-strings.mjs @@ -54,8 +54,8 @@ function processVueComponent(filePath, toolName) { } const hasAlreayI18n = filePath.endsWith('.vue') - ? content.includes("const { t } = useI18n();") - : content.includes("import { translate as t } from '@/plugins/i18n.plugin';"); + ? /const\s+\{.*?\bt\b.*?\}\s+=\s+useI18n\(.*?\)/s.test(content) + : /import\s+\{.*?\btranslate\b.*?\}\s+from\s+(['"])@\/plugins\/i18n\.plugin\1/s.test(content); if (hasAlreayI18n) { console.log(`Already extracted: ${filePath}`); return; diff --git a/src/plugins/i18n.plugin.ts b/src/plugins/i18n.plugin.ts index e66ebaaf02..db35b95155 100644 --- a/src/plugins/i18n.plugin.ts +++ b/src/plugins/i18n.plugin.ts @@ -3,9 +3,11 @@ import { get } from '@vueuse/core'; import type { Plugin } from 'vue'; import { createI18n } from 'vue-i18n'; +const DEFAULT_LOCALE = String(import.meta.env.VITE_LANGUAGE || 'en'); + const i18n = createI18n({ legacy: false, - locale: import.meta.env.VITE_LANGUAGE || 'en', + locale: DEFAULT_LOCALE, messages, }); @@ -15,7 +17,8 @@ export const i18nPlugin: Plugin = { }, }; -export const translate = function (localeKey: string, list: unknown[] = []) { - const hasKey = i18n.global.te(localeKey, get(i18n.global.locale)); - return hasKey ? i18n.global.t(localeKey, list) : localeKey; -}; +export function getCurrentLocale(): string { + return get(i18n.global.locale); +} + +export const translate = i18n.global.t as typeof i18n.global.t; diff --git a/src/tools/bcrypt/bcrypt.models.test.ts b/src/tools/bcrypt/bcrypt.models.test.ts new file mode 100644 index 0000000000..7d933410b7 --- /dev/null +++ b/src/tools/bcrypt/bcrypt.models.test.ts @@ -0,0 +1,49 @@ +import { compare, hash } from 'bcryptjs'; +import { assert, describe, expect, test } from 'vitest'; +import { type Update, bcryptWithProgressUpdates } from './bcrypt.models'; + +// simplified polyfill for https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fromAsync +async function fromAsync(iter: AsyncIterable) { + const out: T[] = []; + for await (const val of iter) { + out.push(val); + } + return out; +} + +function checkProgressAndGetResult(updates: Update[]) { + const first = updates.at(0); + const penultimate = updates.at(-2); + const last = updates.at(-1); + const allExceptLast = updates.slice(0, -1); + + expect(allExceptLast.every(x => x.kind === 'progress')).toBeTruthy(); + expect(first).toEqual({ kind: 'progress', progress: 0 }); + expect(penultimate).toEqual({ kind: 'progress', progress: 1 }); + + assert(last != null && last.kind === 'success'); + + return last; +} + +describe('bcrypt models', () => { + describe(bcryptWithProgressUpdates.name, () => { + test('with bcrypt hash function', async () => { + const updates = await fromAsync(bcryptWithProgressUpdates(hash, ['abc', 5])); + const result = checkProgressAndGetResult(updates); + + expect(result.value).toMatch(/^\$2a\$05\$.{53}$/); + expect(result.timeTakenMs).toBeGreaterThan(0); + }); + + test('with bcrypt compare function', async () => { + const updates = await fromAsync( + bcryptWithProgressUpdates(compare, ['abc', '$2a$05$FHzYelm8Qn.IhGP.N8V1TOWFlRTK.8cphbxZSvSFo9B6HGscnQdhy']), + ); + const result = checkProgressAndGetResult(updates); + + expect(result.value).toBe(true); + expect(result.timeTakenMs).toBeGreaterThan(0); + }); + }); +}); diff --git a/src/tools/bcrypt/bcrypt.models.ts b/src/tools/bcrypt/bcrypt.models.ts new file mode 100644 index 0000000000..3ffd5bad70 --- /dev/null +++ b/src/tools/bcrypt/bcrypt.models.ts @@ -0,0 +1,90 @@ +import { getCurrentLocale, translate as t } from '@/plugins/i18n.plugin'; + +export type Update = + | { + kind: 'progress' + progress: number + } + | { + kind: 'success' + value: Result + timeTakenMs: number + } + | { + kind: 'error' + message: string + }; + +// generic type for the callback versions of bcryptjs's `hash` and `compare` +export type BcryptFn = ( + arg1: string, + arg2: Param, + callback: (err: Error | null, hash: Result) => void, + progressCallback: (percent: number) => void, +) => void; + +interface BcryptWithProgressOptions { + signal: AbortSignal + timeoutMs: number +} + +export async function* bcryptWithProgressUpdates( + fn: BcryptFn, + args: [string, Param], + options?: Partial, +): AsyncGenerator, undefined, undefined> { + const { timeoutMs = 10_000 } = options ?? {}; + const signal = AbortSignal.any([ + AbortSignal.timeout(timeoutMs), + options?.signal, + ].filter(x => x != null)); + + let res = (_: Update) => {}; + const nextPromise = () => new Promise>(resolve => res = resolve); + const promises = [nextPromise()]; + const nextValue = (value: Update) => { + res(value); + promises.push(nextPromise()); + }; + + const start = Date.now(); + + fn( + args[0], + args[1], + (err, result) => { + nextValue( + err == null + ? { kind: 'success', value: result, timeTakenMs: Date.now() - start } + : { kind: 'error', message: err.message }, + ); + }, + (progress) => { + if (signal.aborted) { + nextValue({ kind: 'progress', progress: 0 }); + if (signal.reason instanceof DOMException && signal.reason.name === 'TimeoutError') { + const message = t('tools.bcrypt.texts.timed-out-after-timeout-period', { + timeoutPeriod: new Intl.DurationFormat(getCurrentLocale(), { style: 'long' }) + .format({ seconds: Math.round(timeoutMs / 1000) }), + }); + + nextValue({ kind: 'error', message }); + } + + // throw inside callback to cancel execution of hashing/comparing + throw signal.reason; + } + else { + nextValue({ kind: 'progress', progress }); + } + }, + ); + + for await (const value of promises) { + yield value; + + if (value.kind === 'success' || value.kind === 'error') { + return; + } + } +} diff --git a/src/tools/bcrypt/bcrypt.vue b/src/tools/bcrypt/bcrypt.vue index 6b7e14620a..dd37df6df8 100644 --- a/src/tools/bcrypt/bcrypt.vue +++ b/src/tools/bcrypt/bcrypt.vue @@ -1,21 +1,106 @@ - From 368f63fa578e93180bd774866927ddd92abd7283 Mon Sep 17 00:00:00 2001 From: lionel-rowe Date: Mon, 3 Nov 2025 10:48:55 +0800 Subject: [PATCH 3/3] Update bcryptjs to 3.0.3 and remove setInterval workaround --- package.json | 2 +- pnpm-lock.yaml | 11 ++++++----- src/shims.d.ts | 9 +++++++++ src/tools/bcrypt/bcrypt.models.ts | 8 ++++++++ src/tools/bcrypt/bcrypt.vue | 29 +---------------------------- 5 files changed, 25 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 80802e31f4..7965c6c3e1 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "ansible-vault": "^1.3.0", "apache-md5": "^1.1.8", "arr-diff": "^4.0.0", - "bcryptjs": "^2.4.3", + "bcryptjs": "^3.0.3", "big.js": "^6.2.2", "braces": "^3.0.3", "bwip-js": "^4.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d082bd5c5b..5581b4693d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,8 +192,8 @@ importers: specifier: ^4.0.0 version: 4.0.0 bcryptjs: - specifier: ^2.4.3 - version: 2.4.3 + specifier: ^3.0.3 + version: 3.0.3 big.js: specifier: ^6.2.2 version: 6.2.2 @@ -5013,8 +5013,9 @@ packages: bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} - bcryptjs@2.4.3: - resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + bcryptjs@3.0.3: + resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} + hasBin: true bencode@4.0.0: resolution: {integrity: sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==} @@ -16157,7 +16158,7 @@ snapshots: dependencies: tweetnacl: 0.14.5 - bcryptjs@2.4.3: {} + bcryptjs@3.0.3: {} bencode@4.0.0: dependencies: diff --git a/src/shims.d.ts b/src/shims.d.ts index 7f983204a1..decb0592c7 100644 --- a/src/shims.d.ts +++ b/src/shims.d.ts @@ -55,3 +55,12 @@ interface Navigator { interface FontFaceSet { add(fontFace: FontFace) } + +// TODO remove once https://github.com/microsoft/TypeScript/issues/60608 is resolved +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace Intl { + class DurationFormat { + constructor(locale?: Intl.LocalesArgument, options?: { style?: 'long' }); + format(duration: { seconds?: number; milliseconds?: number }): string; + } +} diff --git a/src/tools/bcrypt/bcrypt.models.ts b/src/tools/bcrypt/bcrypt.models.ts index 3ffd5bad70..37fc4bff45 100644 --- a/src/tools/bcrypt/bcrypt.models.ts +++ b/src/tools/bcrypt/bcrypt.models.ts @@ -1,5 +1,13 @@ import { getCurrentLocale, translate as t } from '@/plugins/i18n.plugin'; +Intl.DurationFormat ??= class DurationFormat { + format(duration: { seconds?: number; milliseconds?: number }): string { + return 'seconds' in duration + ? `${duration.seconds} seconds` + : `${duration.milliseconds} milliseconds`; + } +}; + export type Update = | { kind: 'progress' diff --git a/src/tools/bcrypt/bcrypt.vue b/src/tools/bcrypt/bcrypt.vue index dd37df6df8..e9177d451a 100644 --- a/src/tools/bcrypt/bcrypt.vue +++ b/src/tools/bcrypt/bcrypt.vue @@ -1,37 +1,10 @@