@luckystack/server 0.10.0 → 0.10.2

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/CHANGELOG.md CHANGED
@@ -5,7 +5,11 @@ All notable changes to `@luckystack/server` are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.10.2] - 2026-10-09
9
+
10
+ ### Added
11
+
12
+ - `/_test/reset` accepts `?only=<target>[,<target>]` to clear exactly those targets (the shared list in core `TEST_RESET_TARGETS`). An unknown or empty target is `400 test.invalidResetTarget` — a typo must not silently reset nothing, or everything. Without `only` the default reset is unchanged. This lets an authenticated test sweep drain limiter buckets without deleting its own session.
9
13
 
10
14
  ## [0.8.7] - 2026-08-18
11
15
 
package/CLAUDE.md CHANGED
@@ -69,6 +69,7 @@ One-call server bootstrap for a LuckyStack project. Wires together a raw Node.js
69
69
  - `SERVER_IP` (env, optional, default `127.0.0.1`) — bind address fallback when `options.ip` is omitted.
70
70
  - `NODE_ENV` (env, required for security-sensitive branches) — `development` / `test` toggle devkit hot reload + REPL and gate `/_test/reset`.
71
71
  - `TEST_RESET_TOKEN` (env, required for `/_test/reset` to be reachable at all) — must match the `x-test-reset-token` request header. No fallback "no auth" mode.
72
+ - `/_test/reset?only=<target>[,<target>]` (query) — clear only the listed targets (core `TEST_RESET_TARGETS`); unknown/empty → `400 test.invalidResetTarget`. Without `only` the default reset (rate limits + sessions + activeUsers, hooks via `?include=hooks`) is unchanged.
72
73
  - `projectConfig.http.healthEndpoint` (config) — path served by `handleHealthRoute`. Default `/_health`.
73
74
  - `projectConfig.http.liveEndpoint` (config) — path served by `handleLivezRoute`. Default `/livez`.
74
75
  - `projectConfig.http.readyEndpoint` (config) — path served by `handleReadyzRoute`. Default `/readyz`.
package/dist/index.js CHANGED
@@ -391,9 +391,12 @@ import {
391
391
  clearAllRateLimits,
392
392
  getProjectConfig as getProjectConfig4,
393
393
  formatKey,
394
+ isTestResetTarget,
394
395
  redis as redis2,
396
+ TEST_RESET_ONLY_PARAM,
395
397
  tryCatch as tryCatch2
396
398
  } from "@luckystack/core";
399
+ var splitList = (value) => (value ?? "").split(",").map((s) => s.trim()).filter(Boolean);
397
400
  var handleTestResetRoute = async ({ req, res, routePath }) => {
398
401
  if (routePath !== getProjectConfig4().http.testResetEndpoint) return false;
399
402
  const nodeEnv = process.env.NODE_ENV;
@@ -419,9 +422,33 @@ var handleTestResetRoute = async ({ req, res, routePath }) => {
419
422
  res.end(JSON.stringify({ status: "error", errorCode: "auth.forbidden" }));
420
423
  return true;
421
424
  }
425
+ const rawUrl = req.url ?? "/";
426
+ const searchParams = URL.canParse(rawUrl, "http://localhost") ? new URL(rawUrl, "http://localhost").searchParams : new URLSearchParams();
427
+ const onlyParam = searchParams.get(TEST_RESET_ONLY_PARAM);
428
+ let targets;
429
+ if (onlyParam === null) {
430
+ targets = /* @__PURE__ */ new Set(["rateLimits", "sessions", "activeUsers"]);
431
+ if (splitList(searchParams.get("include")).includes("hooks")) targets.add("hooks");
432
+ } else {
433
+ const requested = splitList(onlyParam);
434
+ const unknown = requested.filter((target) => !isTestResetTarget(target));
435
+ if (requested.length === 0 || unknown.length > 0) {
436
+ res.statusCode = 400;
437
+ res.setHeader("Content-Type", "application/json");
438
+ res.end(JSON.stringify({
439
+ status: "error",
440
+ errorCode: "test.invalidResetTarget",
441
+ errorParams: [{ key: "target", value: unknown.join(",") || "(empty)" }]
442
+ }));
443
+ return true;
444
+ }
445
+ targets = new Set(requested.filter(isTestResetTarget));
446
+ }
422
447
  const cleared = [];
423
- await clearAllRateLimits();
424
- cleared.push("rateLimits");
448
+ if (targets.has("rateLimits")) {
449
+ await clearAllRateLimits();
450
+ cleared.push("rateLimits");
451
+ }
425
452
  const sessionPattern = `${formatKey("-session", "")}:*`;
426
453
  const activeUsersPattern = `${formatKey("-activeUsers", "")}:*`;
427
454
  const scanAndDelete = async (pattern, label) => {
@@ -442,11 +469,9 @@ var handleTestResetRoute = async ({ req, res, routePath }) => {
442
469
  if (deleted > 0) cleared.push(label);
443
470
  return deleted;
444
471
  };
445
- await scanAndDelete(sessionPattern, "sessions");
446
- await scanAndDelete(activeUsersPattern, "activeUsers");
447
- const rawUrl = req.url ?? "/";
448
- const includeFlag = URL.canParse(rawUrl, "http://localhost") ? new URL(rawUrl, "http://localhost").searchParams.get("include") ?? "" : "";
449
- if (includeFlag.split(",").map((s) => s.trim()).includes("hooks")) {
472
+ if (targets.has("sessions")) await scanAndDelete(sessionPattern, "sessions");
473
+ if (targets.has("activeUsers")) await scanAndDelete(activeUsersPattern, "activeUsers");
474
+ if (targets.has("hooks")) {
450
475
  clearAllHooks();
451
476
  cleared.push("hooks");
452
477
  }