@rpcbase/server 0.460.0 → 0.462.0

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/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './initServer';
2
2
  export * from './getDerivedKey';
3
3
  export * from './hashPassword';
4
+ export * from './passwordHashStorage';
4
5
  export * from './ssrMiddleware';
5
6
  export * from './email';
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA"}
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { createReadStream, readFileSync } from "node:fs";
9
9
  import { createInterface } from "node:readline";
10
10
  import { AsyncLocalStorage } from "node:async_hooks";
11
11
  import assert$1 from "assert";
12
- import { hkdfSync, scrypt } from "crypto";
12
+ import crypto, { hkdfSync, scrypt } from "crypto";
13
13
  import { createProxyMiddleware } from "http-proxy-middleware";
14
14
  import fs from "node:fs/promises";
15
15
  import { Transform } from "node:stream";
@@ -3401,6 +3401,179 @@ async function hashPassword(password, salt) {
3401
3401
  });
3402
3402
  return derivedKey;
3403
3403
  }
3404
+ const DEFAULT_SCRYPT_N = 8192;
3405
+ const DEFAULT_SCRYPT_R = 8;
3406
+ const DEFAULT_SCRYPT_P = 4;
3407
+ const DEFAULT_SCRYPT_KEYLEN = 64;
3408
+ const DEFAULT_SCRYPT_SALT_BYTES = 16;
3409
+ const DEFAULT_SCRYPT_MAXMEM_BYTES = 256 * 1024 * 1024;
3410
+ const MAX_SCRYPT_MAXMEM_BYTES = 1024 * 1024 * 1024;
3411
+ const MAX_SCRYPT_N = 1048576;
3412
+ const MAX_SCRYPT_R = 64;
3413
+ const MAX_SCRYPT_P = 16;
3414
+ const MAX_SCRYPT_KEYLEN = 128;
3415
+ const MAX_SCRYPT_SALT_BYTES = 64;
3416
+ const parseEnvInt = (value) => {
3417
+ if (typeof value !== "string") return void 0;
3418
+ const trimmed = value.trim();
3419
+ if (!trimmed) return void 0;
3420
+ const parsed = Number(trimmed);
3421
+ if (!Number.isSafeInteger(parsed)) return void 0;
3422
+ return parsed;
3423
+ };
3424
+ const isPowerOfTwo = (value) => (value & value - 1) === 0;
3425
+ const estimateScryptMemoryBytes = ({ N, r, p }) => {
3426
+ return 128 * r * (N + p);
3427
+ };
3428
+ const validateScryptParams = (params) => {
3429
+ const { N, r, p, keylen, saltBytes, maxmemBytes } = params;
3430
+ if (!Number.isSafeInteger(N) || N < 2 || N > MAX_SCRYPT_N || !isPowerOfTwo(N)) {
3431
+ return { ok: false, error: "invalid_scrypt_N" };
3432
+ }
3433
+ if (!Number.isSafeInteger(r) || r < 1 || r > MAX_SCRYPT_R) {
3434
+ return { ok: false, error: "invalid_scrypt_r" };
3435
+ }
3436
+ if (!Number.isSafeInteger(p) || p < 1 || p > MAX_SCRYPT_P) {
3437
+ return { ok: false, error: "invalid_scrypt_p" };
3438
+ }
3439
+ if (!Number.isSafeInteger(keylen) || keylen < 16 || keylen > MAX_SCRYPT_KEYLEN) {
3440
+ return { ok: false, error: "invalid_scrypt_keylen" };
3441
+ }
3442
+ if (!Number.isSafeInteger(saltBytes) || saltBytes < 8 || saltBytes > MAX_SCRYPT_SALT_BYTES) {
3443
+ return { ok: false, error: "invalid_scrypt_salt_bytes" };
3444
+ }
3445
+ if (!Number.isSafeInteger(maxmemBytes) || maxmemBytes < 16 * 1024 * 1024 || maxmemBytes > MAX_SCRYPT_MAXMEM_BYTES) {
3446
+ return { ok: false, error: "invalid_scrypt_maxmem" };
3447
+ }
3448
+ const estimatedMem = estimateScryptMemoryBytes({ N, r, p });
3449
+ if (estimatedMem > maxmemBytes) {
3450
+ return { ok: false, error: "scrypt_params_exceed_maxmem" };
3451
+ }
3452
+ return { ok: true };
3453
+ };
3454
+ const getCurrentMaxmemBytes = (opts) => {
3455
+ const envMaxmemBytes = parseEnvInt(process.env.RB_PASSWORD_SCRYPT_MAXMEM_BYTES);
3456
+ const maxmemBytes = opts?.maxmemBytes ?? envMaxmemBytes ?? DEFAULT_SCRYPT_MAXMEM_BYTES;
3457
+ if (!Number.isSafeInteger(maxmemBytes) || maxmemBytes < 16 * 1024 * 1024 || maxmemBytes > MAX_SCRYPT_MAXMEM_BYTES) {
3458
+ throw new Error("invalid_scrypt_maxmem");
3459
+ }
3460
+ return maxmemBytes;
3461
+ };
3462
+ const getCurrentScryptParams = (opts) => {
3463
+ const envN = parseEnvInt(process.env.RB_PASSWORD_SCRYPT_N);
3464
+ const envR = parseEnvInt(process.env.RB_PASSWORD_SCRYPT_R);
3465
+ const envP = parseEnvInt(process.env.RB_PASSWORD_SCRYPT_P);
3466
+ const envKeylen = parseEnvInt(process.env.RB_PASSWORD_SCRYPT_KEYLEN);
3467
+ const envSaltBytes = parseEnvInt(process.env.RB_PASSWORD_SCRYPT_SALT_BYTES);
3468
+ const params = {
3469
+ N: opts?.N ?? envN ?? DEFAULT_SCRYPT_N,
3470
+ r: opts?.r ?? envR ?? DEFAULT_SCRYPT_R,
3471
+ p: opts?.p ?? envP ?? DEFAULT_SCRYPT_P,
3472
+ keylen: opts?.keylen ?? envKeylen ?? DEFAULT_SCRYPT_KEYLEN,
3473
+ saltBytes: opts?.saltBytes ?? envSaltBytes ?? DEFAULT_SCRYPT_SALT_BYTES,
3474
+ maxmemBytes: getCurrentMaxmemBytes(opts)
3475
+ };
3476
+ const validated = validateScryptParams(params);
3477
+ if (!validated.ok) {
3478
+ throw new Error(validated.error);
3479
+ }
3480
+ return params;
3481
+ };
3482
+ const scryptAsync = async (password, salt, params) => {
3483
+ const { N, r, p, keylen, maxmemBytes } = params;
3484
+ return await new Promise((resolve, reject) => {
3485
+ crypto.scrypt(password, salt, keylen, { N, r, p, maxmem: maxmemBytes }, (err, derivedKey) => {
3486
+ if (err) {
3487
+ reject(err);
3488
+ return;
3489
+ }
3490
+ resolve(derivedKey);
3491
+ });
3492
+ });
3493
+ };
3494
+ const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
3495
+ const parseB64 = (value) => {
3496
+ if (!value) return null;
3497
+ if (!base64Regex.test(value)) return null;
3498
+ if (value.length % 4 !== 0) return null;
3499
+ const buf = Buffer.from(value, "base64");
3500
+ if (buf.length === 0) return null;
3501
+ if (buf.toString("base64") !== value) return null;
3502
+ return buf;
3503
+ };
3504
+ const MAX_STORED_PASSWORD_LENGTH = 1024;
3505
+ const MAX_STORED_PASSWORD_SECTION_LENGTH = 256;
3506
+ const parseStoredScryptHash = (stored) => {
3507
+ if (stored.length > MAX_STORED_PASSWORD_LENGTH) return null;
3508
+ const parts = stored.split("$");
3509
+ if (parts.length !== 5) return null;
3510
+ if (parts[0] !== "" || parts[1] !== "scrypt") return null;
3511
+ const paramsStr = parts[2];
3512
+ const saltB64 = parts[3];
3513
+ const dkB64 = parts[4];
3514
+ if (paramsStr.length > MAX_STORED_PASSWORD_SECTION_LENGTH) return null;
3515
+ if (saltB64.length > MAX_STORED_PASSWORD_SECTION_LENGTH) return null;
3516
+ if (dkB64.length > MAX_STORED_PASSWORD_SECTION_LENGTH) return null;
3517
+ const kvPairs = paramsStr.split(",").filter(Boolean);
3518
+ if (kvPairs.length === 0) return null;
3519
+ const params = /* @__PURE__ */ new Map();
3520
+ for (const pair of kvPairs) {
3521
+ const idx = pair.indexOf("=");
3522
+ if (idx <= 0 || idx === pair.length - 1) return null;
3523
+ const key = pair.slice(0, idx);
3524
+ const valueRaw = pair.slice(idx + 1);
3525
+ if (!/^[a-zA-Z0-9]+$/.test(key)) return null;
3526
+ if (params.has(key)) return null;
3527
+ const value = Number(valueRaw);
3528
+ if (!Number.isSafeInteger(value)) return null;
3529
+ params.set(key, value);
3530
+ }
3531
+ const N = params.get("N");
3532
+ const r = params.get("r");
3533
+ const p = params.get("p");
3534
+ const keylen = params.get("keylen");
3535
+ if (N === void 0 || r === void 0 || p === void 0 || keylen === void 0) return null;
3536
+ if (params.size !== 4) return null;
3537
+ const salt = parseB64(saltB64);
3538
+ const dk = parseB64(dkB64);
3539
+ if (!salt || !dk) return null;
3540
+ if (dk.length !== keylen) return null;
3541
+ if (salt.length < 8 || salt.length > MAX_SCRYPT_SALT_BYTES) return null;
3542
+ if (dk.length < 16 || dk.length > MAX_SCRYPT_KEYLEN) return null;
3543
+ const currentMaxmemBytes = getCurrentMaxmemBytes();
3544
+ const validated = validateScryptParams({
3545
+ N,
3546
+ r,
3547
+ p,
3548
+ keylen,
3549
+ saltBytes: salt.length,
3550
+ maxmemBytes: currentMaxmemBytes
3551
+ });
3552
+ if (!validated.ok) return null;
3553
+ return { N, r, p, keylen, salt, dk };
3554
+ };
3555
+ async function hashPasswordForStorage(password, opts) {
3556
+ const { N, r, p, keylen, saltBytes, maxmemBytes } = getCurrentScryptParams(opts);
3557
+ const salt = crypto.randomBytes(saltBytes);
3558
+ const dk = await scryptAsync(password, salt, { N, r, p, keylen, maxmemBytes });
3559
+ const saltB64 = salt.toString("base64");
3560
+ const dkB64 = dk.toString("base64");
3561
+ return `$scrypt$N=${N},r=${r},p=${p},keylen=${keylen}$${saltB64}$${dkB64}`;
3562
+ }
3563
+ async function verifyPasswordFromStorage(password, stored) {
3564
+ const parsed = parseStoredScryptHash(stored);
3565
+ if (!parsed) return false;
3566
+ const { N, r, p, keylen, salt, dk } = parsed;
3567
+ const maxmemBytes = getCurrentMaxmemBytes();
3568
+ let derivedKey;
3569
+ try {
3570
+ derivedKey = await scryptAsync(password, salt, { N, r, p, keylen, maxmemBytes });
3571
+ } catch {
3572
+ return false;
3573
+ }
3574
+ if (derivedKey.length !== dk.length) return false;
3575
+ return crypto.timingSafeEqual(dk, derivedKey);
3576
+ }
3404
3577
  function createLocation(current, to, state = null, key) {
3405
3578
  const location = {
3406
3579
  pathname: current,
@@ -3795,7 +3968,9 @@ const sendEmail = async (params) => {
3795
3968
  export {
3796
3969
  getDerivedKey,
3797
3970
  hashPassword,
3971
+ hashPasswordForStorage,
3798
3972
  initServer,
3799
3973
  sendEmail,
3800
- ssrMiddleware
3974
+ ssrMiddleware,
3975
+ verifyPasswordFromStorage
3801
3976
  };
@@ -0,0 +1,11 @@
1
+ export type HashPasswordForStorageOptions = Partial<{
2
+ N: number;
3
+ r: number;
4
+ p: number;
5
+ keylen: number;
6
+ saltBytes: number;
7
+ maxmemBytes: number;
8
+ }>;
9
+ export declare function hashPasswordForStorage(password: string, opts?: HashPasswordForStorageOptions): Promise<string>;
10
+ export declare function verifyPasswordFromStorage(password: string, stored: string): Promise<boolean>;
11
+ //# sourceMappingURL=passwordHashStorage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passwordHashStorage.d.ts","sourceRoot":"","sources":["../src/passwordHashStorage.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,6BAA6B,GAAG,OAAO,CAAC;IAClD,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;CACpB,CAAC,CAAA;AA4MF,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,6BAA6B,GAAG,OAAO,CAAC,MAAM,CAAC,CAUpH;AAED,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiBlG"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=passwordHashStorage.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passwordHashStorage.test.d.ts","sourceRoot":"","sources":["../src/passwordHashStorage.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/server",
3
- "version": "0.460.0",
3
+ "version": "0.462.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -9,6 +9,7 @@
9
9
  "types": "./dist/index.d.ts",
10
10
  "scripts": {
11
11
  "build": "wireit",
12
+ "test": "wireit",
12
13
  "release": "wireit"
13
14
  },
14
15
  "wireit": {
@@ -30,6 +31,15 @@
30
31
  "command": "../../node_modules/.bin/vite build --watch",
31
32
  "service": true
32
33
  },
34
+ "test": {
35
+ "command": "../../node_modules/.bin/vitest run --config ../../pkg/test/src/vitest.config.mjs --passWithNoTests",
36
+ "files": [
37
+ "src/**/*",
38
+ "tsconfig.json",
39
+ "../../scripts/tsconfig.pkg.json"
40
+ ],
41
+ "dependencies": []
42
+ },
33
43
  "release": {
34
44
  "command": "../../scripts/publish.js",
35
45
  "dependencies": [