@rpcbase/server 0.461.0 → 0.463.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.
@@ -1,4 +1,9 @@
1
1
  import { Request } from 'express';
2
2
  import { StaticHandlerContext } from '../../router/src';
3
- export declare function applyRouteLoaders(req: Request, dataRoutes: any[]): Promise<StaticHandlerContext>;
3
+ export type RouterContextWithRedirect = StaticHandlerContext & {
4
+ redirectResponse?: Response;
5
+ redirectRouteId?: string | null;
6
+ redirectRoutePath?: string | null;
7
+ };
8
+ export declare function applyRouteLoaders(req: Request, dataRoutes: any[]): Promise<RouterContextWithRedirect>;
4
9
  //# sourceMappingURL=applyRouteLoaders.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"applyRouteLoaders.d.ts","sourceRoot":"","sources":["../src/applyRouteLoaders.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,EAAC,MAAM,SAAS,CAAA;AAC/B,OAAO,EACL,oBAAoB,EAMrB,MAAM,iBAAiB,CAAA;AAqExB,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,UAAU,EAAE,GAAG,EAAE,GAChB,OAAO,CAAC,oBAAoB,CAAC,CAsI/B"}
1
+ {"version":3,"file":"applyRouteLoaders.d.ts","sourceRoot":"","sources":["../src/applyRouteLoaders.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,EAAC,MAAM,SAAS,CAAA;AAC/B,OAAO,EACL,oBAAoB,EAMrB,MAAM,iBAAiB,CAAA;AAuFxB,MAAM,MAAM,yBAAyB,GAAG,oBAAoB,GAAG;IAC7D,gBAAgB,CAAC,EAAE,QAAQ,CAAA;IAC3B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAClC,CAAA;AAED,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,UAAU,EAAE,GAAG,EAAE,GAChB,OAAO,CAAC,yBAAyB,CAAC,CA6KpC"}
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,
@@ -3442,6 +3615,15 @@ const getErrorStatus = (error) => {
3442
3615
  if (typeof candidate?.response?.status === "number") return candidate.response.status;
3443
3616
  return void 0;
3444
3617
  };
3618
+ const isResponseLike = (value) => {
3619
+ return Boolean(
3620
+ value && typeof value === "object" && typeof value.status === "number" && value.headers && typeof value.headers.get === "function"
3621
+ );
3622
+ };
3623
+ const isRedirectResponse = (value) => {
3624
+ if (!isResponseLike(value)) return false;
3625
+ return value.status >= 300 && value.status < 400 && Boolean(value.headers.get("Location"));
3626
+ };
3445
3627
  async function applyRouteLoaders(req, dataRoutes) {
3446
3628
  const baseUrl = `${req.protocol}://${req.get("host")}`;
3447
3629
  const url = new URL(req.originalUrl, baseUrl);
@@ -3486,8 +3668,12 @@ async function applyRouteLoaders(req, dataRoutes) {
3486
3668
  timeoutId = setTimeout(() => {
3487
3669
  const err = new Error(`Loader timeout after ${LOADER_TIMEOUT_MS}ms`);
3488
3670
  err.status = 504;
3489
- console.error("[rpcbase timeout][server loader]", { routeId: route.id, ms: LOADER_TIMEOUT_MS, url: req.originalUrl });
3490
- reject({ id: route.id, reason: err });
3671
+ console.error("[rpcbase timeout][server loader]", {
3672
+ routeId: route.id,
3673
+ ms: LOADER_TIMEOUT_MS,
3674
+ url: req.originalUrl
3675
+ });
3676
+ reject({ id: route.id, path: route.path, reason: err });
3491
3677
  }, LOADER_TIMEOUT_MS);
3492
3678
  });
3493
3679
  const loaderPromise = (async () => {
@@ -3496,9 +3682,9 @@ async function applyRouteLoaders(req, dataRoutes) {
3496
3682
  params,
3497
3683
  ctx: { req }
3498
3684
  });
3499
- return { id: route.id, data };
3685
+ return { id: route.id, path: route.path, data };
3500
3686
  } catch (error) {
3501
- throw { id: route.id, reason: error };
3687
+ throw { id: route.id, path: route.path, reason: error };
3502
3688
  } finally {
3503
3689
  if (timeoutId) {
3504
3690
  clearTimeout(timeoutId);
@@ -3514,15 +3700,33 @@ async function applyRouteLoaders(req, dataRoutes) {
3514
3700
  let errors = null;
3515
3701
  let hasNotFoundError = false;
3516
3702
  let hasNonNotFoundError = false;
3703
+ let redirectResponse = null;
3704
+ let redirectRouteId = null;
3705
+ let redirectRoutePath = null;
3517
3706
  for (const result of loaderPromisesResults) {
3518
3707
  if (result.status === "fulfilled") {
3519
3708
  if (result.value) {
3520
- loaderData[result.value.id] = result.value.data;
3709
+ if (isRedirectResponse(result.value.data)) {
3710
+ redirectResponse = result.value.data;
3711
+ redirectRouteId = result.value.id;
3712
+ redirectRoutePath = result.value.path ?? null;
3713
+ } else {
3714
+ loaderData[result.value.id] = result.value.data;
3715
+ }
3521
3716
  }
3522
3717
  } else if (result.status === "rejected") {
3523
3718
  const id = result.reason?.id;
3524
3719
  if (!id) {
3525
- throw new Error(`missing route ID in error: ${result.reason}`);
3720
+ throw new Error(
3721
+ `missing route ID in error: ${result.reason}`
3722
+ );
3723
+ }
3724
+ const reasonCandidate = result.reason?.reason ?? result.reason;
3725
+ if (isRedirectResponse(reasonCandidate)) {
3726
+ redirectResponse = reasonCandidate;
3727
+ redirectRouteId = id;
3728
+ redirectRoutePath = result.reason?.path ?? null;
3729
+ continue;
3526
3730
  }
3527
3731
  if (!errors) {
3528
3732
  errors = {};
@@ -3536,6 +3740,19 @@ async function applyRouteLoaders(req, dataRoutes) {
3536
3740
  errors[id] = result.reason;
3537
3741
  }
3538
3742
  }
3743
+ if (redirectResponse) {
3744
+ return {
3745
+ ...baseContext,
3746
+ matches,
3747
+ loaderData,
3748
+ actionData: null,
3749
+ errors: null,
3750
+ statusCode: redirectResponse.status,
3751
+ redirectResponse,
3752
+ redirectRouteId,
3753
+ redirectRoutePath
3754
+ };
3755
+ }
3539
3756
  let statusCode = 200;
3540
3757
  if (errors) {
3541
3758
  if (hasNonNotFoundError) {
@@ -3556,19 +3773,46 @@ async function applyRouteLoaders(req, dataRoutes) {
3556
3773
  };
3557
3774
  }
3558
3775
  async function renderSSR(req, dataRoutes) {
3559
- let routerContext;
3560
- try {
3561
- routerContext = await applyRouteLoaders(req, dataRoutes);
3562
- } catch (err) {
3563
- console.log(err);
3564
- throw err;
3776
+ const routerContext = await applyRouteLoaders(req, dataRoutes);
3777
+ const isMatched = routerContext.matches.length > 0;
3778
+ if (routerContext.redirectResponse) {
3779
+ return {
3780
+ element: null,
3781
+ isMatched,
3782
+ statusCode: routerContext.statusCode ?? routerContext.redirectResponse.status ?? 302,
3783
+ redirectResponse: routerContext.redirectResponse,
3784
+ redirectRouteId: routerContext.redirectRouteId,
3785
+ redirectRoutePath: routerContext.redirectRoutePath
3786
+ };
3565
3787
  }
3566
3788
  if (routerContext.errors) {
3567
3789
  if (routerContext.statusCode === 404) {
3568
- console.warn(`SSR 404 ${req.method} ${req.originalUrl}`, { errors: routerContext.errors });
3790
+ console.warn(`SSR 404 ${req.method} ${req.originalUrl}`, {
3791
+ errors: routerContext.errors
3792
+ });
3569
3793
  } else {
3570
- console.error(`SSR ${routerContext.statusCode || 500} ${req.method} ${req.originalUrl}`, routerContext.errors);
3571
- const error = new Error("SSR loader error");
3794
+ const matchesSummary = routerContext.matches?.map((m) => ({
3795
+ routeId: m.route.id,
3796
+ routePath: m.route.path,
3797
+ pathname: m.pathname,
3798
+ pathnameBase: m.pathnameBase
3799
+ }));
3800
+ console.error(
3801
+ `SSR ${routerContext.statusCode || 500} ${req.method} ${req.originalUrl}`,
3802
+ {
3803
+ errors: routerContext.errors,
3804
+ matches: matchesSummary
3805
+ }
3806
+ );
3807
+ const errorEntries = Object.entries(routerContext.errors || {});
3808
+ const firstErrorEntry = errorEntries[0]?.[1];
3809
+ const firstReason = firstErrorEntry?.reason ?? firstErrorEntry;
3810
+ if (errorEntries.length === 1 && firstReason instanceof Error) {
3811
+ firstReason.details = routerContext.errors;
3812
+ throw firstReason;
3813
+ }
3814
+ const extra = firstErrorEntry?.id ? ` (route ${firstErrorEntry.id}${firstErrorEntry?.path ? ` ${firstErrorEntry.path}` : ""})` : "";
3815
+ const error = new Error(`SSR loader error${extra}`);
3572
3816
  error.details = routerContext.errors;
3573
3817
  throw error;
3574
3818
  }
@@ -3581,8 +3825,11 @@ async function renderSSR(req, dataRoutes) {
3581
3825
  context: routerContext
3582
3826
  }
3583
3827
  ) });
3584
- const isMatched = routerContext.matches.length > 0;
3585
- return { element, isMatched, statusCode: routerContext.statusCode ?? (routerContext.errors ? 500 : 200) };
3828
+ return {
3829
+ element,
3830
+ isMatched,
3831
+ statusCode: routerContext.statusCode ?? (routerContext.errors ? 500 : 200)
3832
+ };
3586
3833
  }
3587
3834
  const ABORT_DELAY_MS = 1e4;
3588
3835
  const APP_HTML_PLACEHOLDER = "<!--app-html-->";
@@ -3591,6 +3838,33 @@ const FALLBACK_ERROR_TEMPLATE_START = `<!doctype html><html lang="en"><head><met
3591
3838
  const FALLBACK_ERROR_TEMPLATE_END = "</main></body></html>";
3592
3839
  const isProduction = env.NODE_ENV === "production";
3593
3840
  const templateHtml = isProduction ? readFileSync("./build/dist/client/src/client/index.html", "utf-8") : "";
3841
+ const handleRedirectionResponse = (res, redirectResponse, location) => {
3842
+ res.status(redirectResponse.status || 302);
3843
+ try {
3844
+ const headers = redirectResponse.headers;
3845
+ const setCookies = headers.getSetCookie?.();
3846
+ const fallbackSetCookies = [];
3847
+ for (const [key, value] of headers) {
3848
+ if (key.toLowerCase() === "set-cookie") {
3849
+ if (!setCookies?.length) {
3850
+ fallbackSetCookies.push(value);
3851
+ }
3852
+ continue;
3853
+ }
3854
+ res.setHeader(key, value);
3855
+ }
3856
+ if (setCookies?.length) {
3857
+ res.setHeader("Set-Cookie", setCookies);
3858
+ } else if (fallbackSetCookies.length === 1) {
3859
+ res.setHeader("Set-Cookie", fallbackSetCookies[0]);
3860
+ } else if (fallbackSetCookies.length > 1) {
3861
+ res.setHeader("Set-Cookie", fallbackSetCookies);
3862
+ }
3863
+ } catch {
3864
+ }
3865
+ if (location) res.setHeader("Location", location);
3866
+ res.end();
3867
+ };
3594
3868
  const formatErrorDetails = (error) => {
3595
3869
  if (isProduction) return void 0;
3596
3870
  if (!error) return void 0;
@@ -3701,7 +3975,32 @@ const ssrMiddleware = ({
3701
3975
  htmlStart = templateStart;
3702
3976
  }
3703
3977
  htmlEnd = template.slice(placeholderIndex + APP_HTML_PLACEHOLDER.length);
3704
- const { element, isMatched, statusCode } = await renderSSR(req, dataRoutes);
3978
+ const {
3979
+ element,
3980
+ isMatched,
3981
+ statusCode,
3982
+ redirectResponse,
3983
+ redirectRouteId,
3984
+ redirectRoutePath
3985
+ } = await renderSSR(req, dataRoutes);
3986
+ if (redirectResponse) {
3987
+ if (!responseCommitted) {
3988
+ const location = redirectResponse.headers?.get?.("Location");
3989
+ if (!isProduction) {
3990
+ console.info("SSR redirect", {
3991
+ method: req.method,
3992
+ url: req.originalUrl,
3993
+ status: redirectResponse.status,
3994
+ location,
3995
+ routeId: redirectRouteId ?? void 0,
3996
+ routePath: redirectRoutePath ?? void 0
3997
+ });
3998
+ }
3999
+ responseCommitted = true;
4000
+ handleRedirectionResponse(res, redirectResponse, location);
4001
+ }
4002
+ return;
4003
+ }
3705
4004
  if (!isMatched) {
3706
4005
  next();
3707
4006
  return;
@@ -3795,7 +4094,9 @@ const sendEmail = async (params) => {
3795
4094
  export {
3796
4095
  getDerivedKey,
3797
4096
  hashPassword,
4097
+ hashPasswordForStorage,
3798
4098
  initServer,
3799
4099
  sendEmail,
3800
- ssrMiddleware
4100
+ ssrMiddleware,
4101
+ verifyPasswordFromStorage
3801
4102
  };
@@ -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":""}
@@ -2,8 +2,11 @@ import { ReactNode } from 'react';
2
2
  import { StaticHandler } from '../../router/src';
3
3
  import * as express from "express";
4
4
  export declare function renderSSR(req: express.Request, dataRoutes: StaticHandler["dataRoutes"]): Promise<{
5
- element: ReactNode;
5
+ element: ReactNode | null;
6
6
  isMatched: boolean;
7
7
  statusCode: number;
8
+ redirectResponse?: Response;
9
+ redirectRouteId?: string | null;
10
+ redirectRoutePath?: string | null;
8
11
  }>;
9
12
  //# sourceMappingURL=renderSSR.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"renderSSR.d.ts","sourceRoot":"","sources":["../src/renderSSR.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAA;AAClC,OAAO,EAAE,SAAS,EAAc,MAAM,OAAO,CAAA;AAC7C,OAAO,EAGL,aAAa,EACd,MAAM,iBAAiB,CAAA;AAOxB,wBAAsB,SAAS,CAC7B,GAAG,EAAE,OAAO,CAAC,OAAO,EACpB,UAAU,EAAE,aAAa,CAAC,YAAY,CAAC,GACtC,OAAO,CAAC;IAAC,OAAO,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAC,CAAC,CAuCvE"}
1
+ {"version":3,"file":"renderSSR.d.ts","sourceRoot":"","sources":["../src/renderSSR.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAA;AAClC,OAAO,EAAE,SAAS,EAAc,MAAM,OAAO,CAAA;AAC7C,OAAO,EAGL,aAAa,EACd,MAAM,iBAAiB,CAAA;AAOxB,wBAAsB,SAAS,CAC7B,GAAG,EAAE,OAAO,CAAC,OAAO,EACpB,UAAU,EAAE,aAAa,CAAC,YAAY,CAAC,GACtC,OAAO,CAAC;IACT,OAAO,EAAE,SAAS,GAAG,IAAI,CAAA;IACzB,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,QAAQ,CAAA;IAC3B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAClC,CAAC,CAwED"}
@@ -1 +1 @@
1
- {"version":3,"file":"ssrMiddleware.d.ts","sourceRoot":"","sources":["../src/ssrMiddleware.ts"],"names":[],"mappings":"AAKA,OAAO,EAAwE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AACjI,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,EAA6B,KAAK,aAAa,EAAE,MAAM,OAAO,CAAA;AAErE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAM3C,KAAK,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC;AAwF9D,eAAO,MAAM,aAAa,GAAI,kFAM3B;IACD,YAAY,EAAE,aAAa,CAAC;IAC5B,UAAU,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACxC,mBAAmB,CAAC,EAAE,aAAa,CAAC;QAAE,KAAK,CAAC,EAAE,oBAAoB,CAAA;KAAE,CAAC,CAAC;IACtE,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/E,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC1B,MAAW,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,kBA8I1D,CAAA"}
1
+ {"version":3,"file":"ssrMiddleware.d.ts","sourceRoot":"","sources":["../src/ssrMiddleware.ts"],"names":[],"mappings":"AAKA,OAAO,EAAwE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AACjI,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,EAA6B,KAAK,aAAa,EAAE,MAAM,OAAO,CAAA;AAErE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAM3C,KAAK,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC;AAkI9D,eAAO,MAAM,aAAa,GAAI,kFAM3B;IACD,YAAY,EAAE,aAAa,CAAC;IAC5B,UAAU,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACxC,mBAAmB,CAAC,EAAE,aAAa,CAAC;QAAE,KAAK,CAAC,EAAE,oBAAoB,CAAA;KAAE,CAAC,CAAC;IACtE,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/E,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC1B,MAAW,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,kBA0K1D,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/server",
3
- "version": "0.461.0",
3
+ "version": "0.463.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": [