@dudousxd/nestjs-durable-dashboard 0.31.0 → 0.32.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.
@@ -27,8 +27,11 @@ __export(index_exports, {
27
27
  DashboardService: () => DashboardService,
28
28
  DurableApiController: () => DurableApiController,
29
29
  DurableApiModule: () => DurableApiModule,
30
+ DurableApiSessionGuard: () => DurableApiSessionGuard,
31
+ DurableAuthController: () => DurableAuthController,
30
32
  DurableDashboardModule: () => DurableDashboardModule,
31
- DurableUiController: () => DurableUiController
33
+ DurableUiController: () => DurableUiController,
34
+ DurableUiSessionGuard: () => DurableUiSessionGuard
32
35
  });
33
36
  module.exports = __toCommonJS(index_exports);
34
37
 
@@ -554,15 +557,468 @@ DurableApiController = _ts_decorate2([
554
557
  ])
555
558
  ], DurableApiController);
556
559
 
557
- // src/server/durable-dashboard.module.ts
558
- var import_reflect_metadata = require("reflect-metadata");
559
- var import_common4 = require("@nestjs/common");
560
- var import_core = require("@nestjs/core");
560
+ // src/server/durable-auth.controller.ts
561
+ var import_common5 = require("@nestjs/common");
562
+
563
+ // src/server/auth/dashboard-auth-config.ts
564
+ var DASHBOARD_AUTH = Symbol("DASHBOARD_AUTH");
565
+ var DEFAULT_TTL = "8h";
566
+ var DEFAULT_TTL_MS = 8 * 60 * 60 * 1e3;
567
+ var DURATION_UNITS = {
568
+ s: 1e3,
569
+ m: 60 * 1e3,
570
+ h: 60 * 60 * 1e3,
571
+ d: 24 * 60 * 60 * 1e3
572
+ };
573
+ function durationToMs(ttl) {
574
+ const match = /^(\d+)([smhd])$/.exec(ttl.trim());
575
+ if (!match) return DEFAULT_TTL_MS;
576
+ const unit = DURATION_UNITS[match[2] ?? ""];
577
+ if (unit === void 0) return DEFAULT_TTL_MS;
578
+ return Number(match[1]) * unit;
579
+ }
580
+ __name(durationToMs, "durationToMs");
581
+ function resolveDashboardAuth(options) {
582
+ if (options === void 0) return null;
583
+ if (typeof options.secret !== "string" || options.secret === "") {
584
+ throw new Error("DurableDashboardModule: dashboardAuth.secret is required and must be a non-empty string (HMAC-SHA256 signing key, 32+ bytes recommended). Failing closed.");
585
+ }
586
+ if (typeof options.login !== "function") {
587
+ throw new Error("DurableDashboardModule: dashboardAuth.login is required (a login hook).");
588
+ }
589
+ return {
590
+ secret: options.secret,
591
+ ttlMs: durationToMs(options.ttl ?? DEFAULT_TTL),
592
+ login: options.login
593
+ };
594
+ }
595
+ __name(resolveDashboardAuth, "resolveDashboardAuth");
596
+
597
+ // src/server/auth/login-page.ts
598
+ function renderLoginPage(basePath) {
599
+ const loginAction = `${basePath}/login`;
600
+ const defaultReturnTo = basePath;
601
+ return `<!doctype html>
602
+ <html lang="en">
603
+ <head>
604
+ <meta charset="utf-8" />
605
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
606
+ <meta name="robots" content="noindex, nofollow" />
607
+ <title>Sign in \u2014 Durable</title>
608
+ <style>
609
+ :root { color-scheme: light dark; }
610
+ * { box-sizing: border-box; }
611
+ body {
612
+ margin: 0;
613
+ min-height: 100vh;
614
+ display: flex;
615
+ align-items: center;
616
+ justify-content: center;
617
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
618
+ background: #0b0f14;
619
+ color: #e5e7eb;
620
+ }
621
+ main {
622
+ width: 100%;
623
+ max-width: 360px;
624
+ padding: 32px;
625
+ }
626
+ h1 {
627
+ font-size: 18px;
628
+ font-weight: 600;
629
+ margin: 0 0 4px;
630
+ }
631
+ p.subtitle {
632
+ margin: 0 0 24px;
633
+ color: #9ca3af;
634
+ font-size: 13px;
635
+ }
636
+ label {
637
+ display: block;
638
+ font-size: 12px;
639
+ font-weight: 500;
640
+ margin-bottom: 6px;
641
+ color: #cbd5e1;
642
+ }
643
+ input {
644
+ width: 100%;
645
+ padding: 8px 10px;
646
+ margin-bottom: 16px;
647
+ border-radius: 6px;
648
+ border: 1px solid #374151;
649
+ background: #111827;
650
+ color: #e5e7eb;
651
+ font-size: 14px;
652
+ }
653
+ input:focus { outline: 2px solid #3b82f6; outline-offset: 1px; }
654
+ button {
655
+ width: 100%;
656
+ padding: 9px 10px;
657
+ border-radius: 6px;
658
+ border: none;
659
+ background: #3b82f6;
660
+ color: white;
661
+ font-size: 14px;
662
+ font-weight: 600;
663
+ cursor: pointer;
664
+ }
665
+ button:disabled { opacity: 0.6; cursor: default; }
666
+ button:hover:not(:disabled) { background: #2563eb; }
667
+ #error {
668
+ display: none;
669
+ margin-bottom: 16px;
670
+ padding: 8px 10px;
671
+ border-radius: 6px;
672
+ background: #7f1d1d;
673
+ color: #fecaca;
674
+ font-size: 13px;
675
+ }
676
+ </style>
677
+ </head>
678
+ <body>
679
+ <main>
680
+ <h1>Durable control plane</h1>
681
+ <p class="subtitle">Sign in to continue.</p>
682
+ <div id="error" role="alert">Invalid username or password.</div>
683
+ <form id="login-form" autocomplete="on">
684
+ <label for="username">Username</label>
685
+ <input id="username" name="username" type="text" autocomplete="username" required autofocus />
686
+ <label for="password">Password</label>
687
+ <input id="password" name="password" type="password" autocomplete="current-password" required />
688
+ <button id="submit" type="submit">Sign in</button>
689
+ </form>
690
+ </main>
691
+ <script>
692
+ (function () {
693
+ var LOGIN_ACTION = ${JSON.stringify(loginAction)};
694
+ var DEFAULT_RETURN_TO = ${JSON.stringify(defaultReturnTo)};
695
+ var params = new URLSearchParams(window.location.search);
696
+ var errorBox = document.getElementById('error');
697
+ if (params.get('error')) errorBox.style.display = 'block';
698
+
699
+ function sameOriginReturnTo(value) {
700
+ if (typeof value !== 'string' || value === '') return DEFAULT_RETURN_TO;
701
+ if (value.charAt(0) !== '/' || value.charAt(1) === '/' || value.indexOf('://') !== -1) {
702
+ return DEFAULT_RETURN_TO;
703
+ }
704
+ return value;
705
+ }
706
+
707
+ var form = document.getElementById('login-form');
708
+ var submitButton = document.getElementById('submit');
709
+ form.addEventListener('submit', function (event) {
710
+ event.preventDefault();
711
+ submitButton.disabled = true;
712
+ errorBox.style.display = 'none';
713
+ var returnTo = sameOriginReturnTo(params.get('returnTo'));
714
+ fetch(LOGIN_ACTION, {
715
+ method: 'POST',
716
+ headers: { 'content-type': 'application/json' },
717
+ credentials: 'same-origin',
718
+ body: JSON.stringify({
719
+ username: document.getElementById('username').value,
720
+ password: document.getElementById('password').value,
721
+ returnTo: returnTo,
722
+ }),
723
+ })
724
+ .then(function (response) {
725
+ if (!response.ok) throw new Error('unauthorized');
726
+ return response.json();
727
+ })
728
+ .then(function (data) {
729
+ window.location.href = data.redirectTo || DEFAULT_RETURN_TO;
730
+ })
731
+ .catch(function () {
732
+ errorBox.style.display = 'block';
733
+ submitButton.disabled = false;
734
+ });
735
+ });
736
+ })();
737
+ </script>
738
+ </body>
739
+ </html>`;
740
+ }
741
+ __name(renderLoginPage, "renderLoginPage");
742
+
743
+ // src/server/auth/request.ts
744
+ function isRecord(value) {
745
+ return typeof value === "object" && value !== null;
746
+ }
747
+ __name(isRecord, "isRecord");
748
+ function readHeaders(request) {
749
+ if (!isRecord(request)) return {};
750
+ const headers = request.headers;
751
+ return isRecord(headers) ? headers : {};
752
+ }
753
+ __name(readHeaders, "readHeaders");
754
+ function headerToString(value) {
755
+ if (typeof value === "string") return value;
756
+ if (Array.isArray(value) && value.every((part) => typeof part === "string")) {
757
+ return value.join(",");
758
+ }
759
+ return void 0;
760
+ }
761
+ __name(headerToString, "headerToString");
762
+ function readCookieHeader(request) {
763
+ return headerToString(readHeaders(request).cookie);
764
+ }
765
+ __name(readCookieHeader, "readCookieHeader");
766
+ function isHttpsRequest(request) {
767
+ if (isRecord(request) && request.protocol === "https") return true;
768
+ const forwarded = headerToString(readHeaders(request)["x-forwarded-proto"]);
769
+ if (forwarded === void 0) return false;
770
+ return forwarded.split(",").map((proto) => proto.trim().toLowerCase()).includes("https");
771
+ }
772
+ __name(isHttpsRequest, "isHttpsRequest");
773
+ function originalRequestUrl(request) {
774
+ if (!isRecord(request)) return "/";
775
+ if (typeof request.originalUrl === "string" && request.originalUrl !== "") {
776
+ return request.originalUrl;
777
+ }
778
+ if (typeof request.url === "string" && request.url !== "") return request.url;
779
+ return "/";
780
+ }
781
+ __name(originalRequestUrl, "originalRequestUrl");
782
+ function sanitizeReturnTo(candidate, fallback) {
783
+ if (typeof candidate !== "string") return fallback;
784
+ if (!candidate.startsWith("/")) return fallback;
785
+ if (candidate.startsWith("//")) return fallback;
786
+ if (candidate.includes("://")) return fallback;
787
+ return candidate;
788
+ }
789
+ __name(sanitizeReturnTo, "sanitizeReturnTo");
790
+
791
+ // src/server/auth/response.ts
792
+ function isRecord2(value) {
793
+ return typeof value === "object" && value !== null;
794
+ }
795
+ __name(isRecord2, "isRecord");
796
+ function isRawCookieResponse(value) {
797
+ return isRecord2(value) && typeof value.getHeader === "function" && typeof value.setHeader === "function";
798
+ }
799
+ __name(isRawCookieResponse, "isRawCookieResponse");
800
+ function resolveCookieResponse(response) {
801
+ if (isRawCookieResponse(response)) return response;
802
+ if (isRecord2(response) && isRawCookieResponse(response.raw)) return response.raw;
803
+ return null;
804
+ }
805
+ __name(resolveCookieResponse, "resolveCookieResponse");
806
+ function existingSetCookies(response) {
807
+ const current = response.getHeader("set-cookie");
808
+ if (Array.isArray(current)) return current;
809
+ if (typeof current === "string") return [
810
+ current
811
+ ];
812
+ return [];
813
+ }
814
+ __name(existingSetCookies, "existingSetCookies");
815
+ function appendSetCookie(response, cookie) {
816
+ const raw = resolveCookieResponse(response);
817
+ if (!raw) return;
818
+ raw.setHeader("set-cookie", [
819
+ ...existingSetCookies(raw),
820
+ cookie
821
+ ]);
822
+ }
823
+ __name(appendSetCookie, "appendSetCookie");
824
+ function isRawRedirectResponse(value) {
825
+ return isRecord2(value) && typeof value.getHeader === "function" && typeof value.setHeader === "function" && typeof value.end === "function";
826
+ }
827
+ __name(isRawRedirectResponse, "isRawRedirectResponse");
828
+ function resolveRedirectResponse(response) {
829
+ if (isRawRedirectResponse(response)) return response;
830
+ if (isRecord2(response) && isRawRedirectResponse(response.raw)) return response.raw;
831
+ return null;
832
+ }
833
+ __name(resolveRedirectResponse, "resolveRedirectResponse");
834
+ function redirectRaw(response, location, status = 302) {
835
+ const raw = resolveRedirectResponse(response);
836
+ if (!raw) return;
837
+ raw.statusCode = status;
838
+ raw.setHeader("location", location);
839
+ raw.end();
840
+ }
841
+ __name(redirectRaw, "redirectRaw");
842
+
843
+ // src/server/auth/cookie-header.ts
844
+ function parseCookieHeader(header) {
845
+ const cookies = {};
846
+ if (typeof header !== "string" || header === "") return cookies;
847
+ for (const segment of header.split(";")) {
848
+ const eq = segment.indexOf("=");
849
+ if (eq <= 0) continue;
850
+ const name = segment.slice(0, eq).trim();
851
+ if (name === "") continue;
852
+ let rawValue = segment.slice(eq + 1).trim();
853
+ if (rawValue.length >= 2 && rawValue.startsWith('"') && rawValue.endsWith('"')) {
854
+ rawValue = rawValue.slice(1, -1);
855
+ }
856
+ if (name in cookies) continue;
857
+ try {
858
+ cookies[name] = decodeURIComponent(rawValue);
859
+ } catch {
860
+ cookies[name] = rawValue;
861
+ }
862
+ }
863
+ return cookies;
864
+ }
865
+ __name(parseCookieHeader, "parseCookieHeader");
866
+ function serializeSetCookie(name, value, options) {
867
+ const parts = [
868
+ `${name}=${options.clear ? "" : encodeURIComponent(value)}`
869
+ ];
870
+ parts.push(`Path=${options.path}`);
871
+ parts.push("HttpOnly");
872
+ parts.push("SameSite=Lax");
873
+ if (options.secure) parts.push("Secure");
874
+ if (options.clear) {
875
+ parts.push("Max-Age=0");
876
+ parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
877
+ } else {
878
+ parts.push(`Max-Age=${options.maxAgeSeconds}`);
879
+ }
880
+ return parts.join("; ");
881
+ }
882
+ __name(serializeSetCookie, "serializeSetCookie");
883
+
884
+ // src/server/auth/session-cookie.ts
885
+ var import_node_crypto = require("crypto");
886
+ var EXP_GRACE_MS = 3e4;
887
+ function base64urlEncode(value) {
888
+ return Buffer.from(value, "utf8").toString("base64url");
889
+ }
890
+ __name(base64urlEncode, "base64urlEncode");
891
+ function base64urlDecode(value) {
892
+ return Buffer.from(value, "base64url").toString("utf8");
893
+ }
894
+ __name(base64urlDecode, "base64urlDecode");
895
+ function hmac(secret, payload) {
896
+ return (0, import_node_crypto.createHmac)("sha256", secret).update(payload).digest("base64url");
897
+ }
898
+ __name(hmac, "hmac");
899
+ function signSessionCookie(user, options) {
900
+ const issuedAt = options.now ?? Date.now();
901
+ const session = {
902
+ sub: user.id,
903
+ ...user.name !== void 0 ? {
904
+ name: user.name
905
+ } : {},
906
+ roles: user.roles ?? [],
907
+ iat: issuedAt,
908
+ exp: issuedAt + options.ttlMs
909
+ };
910
+ const encodedPayload = base64urlEncode(JSON.stringify(session));
911
+ const signature = hmac(options.secret, encodedPayload);
912
+ return `${encodedPayload}.${signature}`;
913
+ }
914
+ __name(signSessionCookie, "signSessionCookie");
915
+ function isSessionPayload(value) {
916
+ if (typeof value !== "object" || value === null) return false;
917
+ const record = {
918
+ ...value
919
+ };
920
+ if (typeof record.sub !== "string") return false;
921
+ if (record.name !== void 0 && typeof record.name !== "string") return false;
922
+ if (!Array.isArray(record.roles)) return false;
923
+ if (!record.roles.every((role) => typeof role === "string")) return false;
924
+ if (typeof record.iat !== "number" || !Number.isFinite(record.iat)) return false;
925
+ if (typeof record.exp !== "number" || !Number.isFinite(record.exp)) return false;
926
+ return true;
927
+ }
928
+ __name(isSessionPayload, "isSessionPayload");
929
+ function verifySessionCookie(value, options) {
930
+ try {
931
+ const dot = value.indexOf(".");
932
+ if (dot <= 0 || dot === value.length - 1) return null;
933
+ const encodedPayload = value.slice(0, dot);
934
+ const providedSignature = value.slice(dot + 1);
935
+ const expectedSignature = hmac(options.secret, encodedPayload);
936
+ const provided = Buffer.from(providedSignature, "base64url");
937
+ const expected = Buffer.from(expectedSignature, "base64url");
938
+ if (provided.length !== expected.length) return null;
939
+ if (!(0, import_node_crypto.timingSafeEqual)(provided, expected)) return null;
940
+ const decoded = JSON.parse(base64urlDecode(encodedPayload));
941
+ if (!isSessionPayload(decoded)) return null;
942
+ const now = options.now ?? Date.now();
943
+ if (now > decoded.exp + EXP_GRACE_MS) return null;
944
+ const session = {
945
+ sub: decoded.sub,
946
+ ...decoded.name !== void 0 ? {
947
+ name: decoded.name
948
+ } : {},
949
+ roles: decoded.roles,
950
+ iat: decoded.iat,
951
+ exp: decoded.exp
952
+ };
953
+ return session;
954
+ } catch {
955
+ return null;
956
+ }
957
+ }
958
+ __name(verifySessionCookie, "verifySessionCookie");
959
+
960
+ // src/server/auth/session-cookie-io.ts
961
+ var SESSION_COOKIE_NAME = "durable_dashboard_session";
962
+ var COOKIE_PATH = "/";
963
+ function issueSessionCookie(user, context) {
964
+ const value = signSessionCookie(user, {
965
+ secret: context.auth.secret,
966
+ ttlMs: context.auth.ttlMs,
967
+ ...context.now !== void 0 ? {
968
+ now: context.now
969
+ } : {}
970
+ });
971
+ const cookie = serializeSetCookie(SESSION_COOKIE_NAME, value, {
972
+ path: COOKIE_PATH,
973
+ maxAgeSeconds: Math.floor(context.auth.ttlMs / 1e3),
974
+ secure: isHttpsRequest(context.request)
975
+ });
976
+ appendSetCookie(context.response, cookie);
977
+ }
978
+ __name(issueSessionCookie, "issueSessionCookie");
979
+ function clearSessionCookie(context) {
980
+ const cookie = serializeSetCookie(SESSION_COOKIE_NAME, "", {
981
+ path: COOKIE_PATH,
982
+ maxAgeSeconds: 0,
983
+ secure: isHttpsRequest(context.request),
984
+ clear: true
985
+ });
986
+ appendSetCookie(context.response, cookie);
987
+ }
988
+ __name(clearSessionCookie, "clearSessionCookie");
989
+ function readSessionFromRequest(auth, request) {
990
+ const cookieValue = parseCookieHeader(readCookieHeader(request))[SESSION_COOKIE_NAME];
991
+ if (cookieValue === void 0) return null;
992
+ return verifySessionCookie(cookieValue, {
993
+ secret: auth.secret
994
+ });
995
+ }
996
+ __name(readSessionFromRequest, "readSessionFromRequest");
997
+ function maybeRenewSession(auth, session, request, response) {
998
+ const now = Date.now();
999
+ if (now - session.iat <= auth.ttlMs / 2) return;
1000
+ issueSessionCookie({
1001
+ id: session.sub,
1002
+ ...session.name !== void 0 ? {
1003
+ name: session.name
1004
+ } : {},
1005
+ roles: session.roles
1006
+ }, {
1007
+ auth,
1008
+ request,
1009
+ response,
1010
+ now
1011
+ });
1012
+ }
1013
+ __name(maybeRenewSession, "maybeRenewSession");
561
1014
 
562
1015
  // src/server/durable-ui.controller.ts
563
1016
  var import_node_fs = require("fs");
564
1017
  var import_node_path = require("path");
565
1018
  var import_node_url = require("url");
1019
+ var import_common4 = require("@nestjs/common");
1020
+
1021
+ // src/server/auth/login-redirect.exception.ts
566
1022
  var import_common3 = require("@nestjs/common");
567
1023
  function _ts_decorate3(decorators, target, key, desc) {
568
1024
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -571,6 +1027,38 @@ function _ts_decorate3(decorators, target, key, desc) {
571
1027
  return c > 3 && r && Object.defineProperty(target, key, r), r;
572
1028
  }
573
1029
  __name(_ts_decorate3, "_ts_decorate");
1030
+ var DashboardLoginRedirectException = class extends import_common3.HttpException {
1031
+ static {
1032
+ __name(this, "DashboardLoginRedirectException");
1033
+ }
1034
+ location;
1035
+ constructor(location) {
1036
+ super({
1037
+ location
1038
+ }, import_common3.HttpStatus.FOUND), this.location = location;
1039
+ }
1040
+ };
1041
+ var DashboardLoginRedirectFilter = class {
1042
+ static {
1043
+ __name(this, "DashboardLoginRedirectFilter");
1044
+ }
1045
+ catch(exception, host) {
1046
+ const response = host.switchToHttp().getResponse();
1047
+ redirectRaw(response, exception.location);
1048
+ }
1049
+ };
1050
+ DashboardLoginRedirectFilter = _ts_decorate3([
1051
+ (0, import_common3.Catch)(DashboardLoginRedirectException)
1052
+ ], DashboardLoginRedirectFilter);
1053
+
1054
+ // src/server/durable-ui.controller.ts
1055
+ function _ts_decorate4(decorators, target, key, desc) {
1056
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1057
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1058
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1059
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1060
+ }
1061
+ __name(_ts_decorate4, "_ts_decorate");
574
1062
  function _ts_metadata3(k, v) {
575
1063
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
576
1064
  }
@@ -613,7 +1101,7 @@ var DurableUiController = class {
613
1101
  index() {
614
1102
  const indexPath = (0, import_node_path.join)(this.dir, "index.html");
615
1103
  if (!(0, import_node_fs.existsSync)(indexPath)) {
616
- throw new import_common3.NotFoundException("Dashboard is not built. Run the package build.");
1104
+ throw new import_common4.NotFoundException("Dashboard is not built. Run the package build.");
617
1105
  }
618
1106
  const html = (0, import_node_fs.readFileSync)(indexPath, "utf8").replaceAll(`="${BUILD_BASE}/`, `="${this.basePath}/`);
619
1107
  const inject = `<script>window.__DURABLE_BASE__='${this.basePath}';window.__DURABLE_API__='${this.apiBasePath}';</script>`;
@@ -621,40 +1109,41 @@ var DurableUiController = class {
621
1109
  }
622
1110
  asset(file) {
623
1111
  const safe = (0, import_node_path.basename)(file);
624
- if (safe !== file) throw new import_common3.NotFoundException();
1112
+ if (safe !== file) throw new import_common4.NotFoundException();
625
1113
  const root = (0, import_node_path.resolve)(this.dir, "assets");
626
1114
  const assetPath = (0, import_node_path.resolve)(root, safe);
627
1115
  if (!assetPath.startsWith(root + import_node_path.sep) || !(0, import_node_fs.existsSync)(assetPath)) {
628
- throw new import_common3.NotFoundException();
1116
+ throw new import_common4.NotFoundException();
629
1117
  }
630
1118
  const type = CONTENT_TYPES[(0, import_node_path.extname)(safe)] ?? "application/octet-stream";
631
- return new import_common3.StreamableFile((0, import_node_fs.readFileSync)(assetPath), {
1119
+ return new import_common4.StreamableFile((0, import_node_fs.readFileSync)(assetPath), {
632
1120
  type
633
1121
  });
634
1122
  }
635
1123
  };
636
- _ts_decorate3([
637
- (0, import_common3.Get)(),
638
- (0, import_common3.Header)("Content-Type", "text/html; charset=utf-8"),
639
- (0, import_common3.Header)("Cache-Control", "no-store, must-revalidate"),
1124
+ _ts_decorate4([
1125
+ (0, import_common4.Get)(),
1126
+ (0, import_common4.Header)("Content-Type", "text/html; charset=utf-8"),
1127
+ (0, import_common4.Header)("Cache-Control", "no-store, must-revalidate"),
640
1128
  _ts_metadata3("design:type", Function),
641
1129
  _ts_metadata3("design:paramtypes", []),
642
1130
  _ts_metadata3("design:returntype", String)
643
1131
  ], DurableUiController.prototype, "index", null);
644
- _ts_decorate3([
645
- (0, import_common3.Get)("assets/:file"),
646
- (0, import_common3.Header)("Cache-Control", "public, max-age=31536000, immutable"),
647
- _ts_param3(0, (0, import_common3.Param)("file")),
1132
+ _ts_decorate4([
1133
+ (0, import_common4.Get)("assets/:file"),
1134
+ (0, import_common4.Header)("Cache-Control", "public, max-age=31536000, immutable"),
1135
+ _ts_param3(0, (0, import_common4.Param)("file")),
648
1136
  _ts_metadata3("design:type", Function),
649
1137
  _ts_metadata3("design:paramtypes", [
650
1138
  String
651
1139
  ]),
652
- _ts_metadata3("design:returntype", typeof import_common3.StreamableFile === "undefined" ? Object : import_common3.StreamableFile)
1140
+ _ts_metadata3("design:returntype", typeof import_common4.StreamableFile === "undefined" ? Object : import_common4.StreamableFile)
653
1141
  ], DurableUiController.prototype, "asset", null);
654
- DurableUiController = _ts_decorate3([
655
- (0, import_common3.Controller)(),
656
- _ts_param3(0, (0, import_common3.Inject)(DASHBOARD_BASE_PATH)),
657
- _ts_param3(1, (0, import_common3.Inject)(DASHBOARD_API_PATH)),
1142
+ DurableUiController = _ts_decorate4([
1143
+ (0, import_common4.UseFilters)(DashboardLoginRedirectFilter),
1144
+ (0, import_common4.Controller)(),
1145
+ _ts_param3(0, (0, import_common4.Inject)(DASHBOARD_BASE_PATH)),
1146
+ _ts_param3(1, (0, import_common4.Inject)(DASHBOARD_API_PATH)),
658
1147
  _ts_metadata3("design:type", Function),
659
1148
  _ts_metadata3("design:paramtypes", [
660
1149
  String,
@@ -662,23 +1151,254 @@ DurableUiController = _ts_decorate3([
662
1151
  ])
663
1152
  ], DurableUiController);
664
1153
 
1154
+ // src/server/durable-auth.controller.ts
1155
+ function _ts_decorate5(decorators, target, key, desc) {
1156
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1157
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1158
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1159
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1160
+ }
1161
+ __name(_ts_decorate5, "_ts_decorate");
1162
+ function _ts_metadata4(k, v) {
1163
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1164
+ }
1165
+ __name(_ts_metadata4, "_ts_metadata");
1166
+ function _ts_param4(paramIndex, decorator) {
1167
+ return function(target, key) {
1168
+ decorator(target, key, paramIndex);
1169
+ };
1170
+ }
1171
+ __name(_ts_param4, "_ts_param");
1172
+ function isString(value) {
1173
+ return typeof value === "string";
1174
+ }
1175
+ __name(isString, "isString");
1176
+ var DurableAuthController = class _DurableAuthController {
1177
+ static {
1178
+ __name(this, "DurableAuthController");
1179
+ }
1180
+ auth;
1181
+ basePath;
1182
+ logger = new import_common5.Logger(_DurableAuthController.name);
1183
+ /** Warn once so a throwing `login` hook doesn't spam logs on every failed attempt. */
1184
+ warnedOnHookThrow = false;
1185
+ constructor(auth, basePath) {
1186
+ this.auth = auth;
1187
+ this.basePath = basePath;
1188
+ }
1189
+ // The login page reads `returnTo`/`error` from `location.search` client-side (see
1190
+ // `login-page.ts`), so this response never varies per request and needs no templating beyond
1191
+ // the developer-controlled `basePath`.
1192
+ loginPage() {
1193
+ if (!this.auth) throw new import_common5.NotFoundException();
1194
+ return renderLoginPage(this.basePath);
1195
+ }
1196
+ // Called by the login page's own fetch (JSON, not a classic form POST — see `login-page.ts` for
1197
+ // why: it keeps the login page working with no host-configured body parser beyond the
1198
+ // JSON one Nest wires in by default on both Express and Fastify). Uniform `401` on any failure
1199
+ // (unknown user, wrong password, throwing hook) — no user-enumeration.
1200
+ async login(body, request, response) {
1201
+ const auth = this.requireAuth();
1202
+ if (!isString(body?.username) || !isString(body?.password)) {
1203
+ throw new import_common5.BadRequestException("Body must include string `username` and `password`.");
1204
+ }
1205
+ const user = await this.runLoginHook(auth, body.username, body.password);
1206
+ if (!user) throw new import_common5.UnauthorizedException({
1207
+ message: "Invalid username or password."
1208
+ });
1209
+ issueSessionCookie(user, {
1210
+ auth,
1211
+ request,
1212
+ response
1213
+ });
1214
+ return {
1215
+ redirectTo: sanitizeReturnTo(body.returnTo, this.basePath)
1216
+ };
1217
+ }
1218
+ // Plain `GET` (not `POST`): logging out only ever destroys the CALLER's own session, so it's
1219
+ // safe/idempotent from the caller's perspective — a simple `<a href>` (no JS, no CSRF token)
1220
+ // works. Non-passthrough `@Res()`: this handler owns the response outright (clears the cookie,
1221
+ // then redirects), so Nest must not also try to send a return value afterwards.
1222
+ logout(request, response) {
1223
+ clearSessionCookie({
1224
+ request,
1225
+ response
1226
+ });
1227
+ redirectRaw(response, `${this.basePath}/login`);
1228
+ }
1229
+ requireAuth() {
1230
+ if (!this.auth) throw new import_common5.NotFoundException();
1231
+ return this.auth;
1232
+ }
1233
+ /**
1234
+ * Run the host's `login` hook defensively: a throw is treated as a denial (uniform failure) and
1235
+ * warn-logged once, so a buggy hook never 500s the endpoint into a stack-trace leak nor floods
1236
+ * the logs on repeated bad attempts.
1237
+ */
1238
+ async runLoginHook(auth, username, password) {
1239
+ try {
1240
+ return await auth.login(username, password) ?? null;
1241
+ } catch (error) {
1242
+ if (!this.warnedOnHookThrow) {
1243
+ this.warnedOnHookThrow = true;
1244
+ this.logger.warn(`dashboardAuth login hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
1245
+ }
1246
+ return null;
1247
+ }
1248
+ }
1249
+ };
1250
+ _ts_decorate5([
1251
+ (0, import_common5.Get)("login"),
1252
+ (0, import_common5.Header)("Content-Type", "text/html; charset=utf-8"),
1253
+ (0, import_common5.Header)("Cache-Control", "no-store, must-revalidate"),
1254
+ _ts_metadata4("design:type", Function),
1255
+ _ts_metadata4("design:paramtypes", []),
1256
+ _ts_metadata4("design:returntype", String)
1257
+ ], DurableAuthController.prototype, "loginPage", null);
1258
+ _ts_decorate5([
1259
+ (0, import_common5.Post)("login"),
1260
+ (0, import_common5.HttpCode)(200),
1261
+ _ts_param4(0, (0, import_common5.Body)()),
1262
+ _ts_param4(1, (0, import_common5.Req)()),
1263
+ _ts_param4(2, (0, import_common5.Res)({
1264
+ passthrough: true
1265
+ })),
1266
+ _ts_metadata4("design:type", Function),
1267
+ _ts_metadata4("design:paramtypes", [
1268
+ typeof LoginBody === "undefined" ? Object : LoginBody,
1269
+ Object,
1270
+ Object
1271
+ ]),
1272
+ _ts_metadata4("design:returntype", Promise)
1273
+ ], DurableAuthController.prototype, "login", null);
1274
+ _ts_decorate5([
1275
+ (0, import_common5.Get)("logout"),
1276
+ _ts_param4(0, (0, import_common5.Req)()),
1277
+ _ts_param4(1, (0, import_common5.Res)()),
1278
+ _ts_metadata4("design:type", Function),
1279
+ _ts_metadata4("design:paramtypes", [
1280
+ Object,
1281
+ Object
1282
+ ]),
1283
+ _ts_metadata4("design:returntype", void 0)
1284
+ ], DurableAuthController.prototype, "logout", null);
1285
+ DurableAuthController = _ts_decorate5([
1286
+ (0, import_common5.Controller)(),
1287
+ _ts_param4(0, (0, import_common5.Optional)()),
1288
+ _ts_param4(0, (0, import_common5.Inject)(DASHBOARD_AUTH)),
1289
+ _ts_param4(1, (0, import_common5.Inject)(DASHBOARD_BASE_PATH)),
1290
+ _ts_metadata4("design:type", Function),
1291
+ _ts_metadata4("design:paramtypes", [
1292
+ Object,
1293
+ String
1294
+ ])
1295
+ ], DurableAuthController);
1296
+
665
1297
  // src/server/durable-dashboard.module.ts
666
- function _ts_decorate4(decorators, target, key, desc) {
1298
+ var import_reflect_metadata = require("reflect-metadata");
1299
+ var import_common7 = require("@nestjs/common");
1300
+ var import_core = require("@nestjs/core");
1301
+
1302
+ // src/server/durable-session.guard.ts
1303
+ var import_common6 = require("@nestjs/common");
1304
+ function _ts_decorate6(decorators, target, key, desc) {
667
1305
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
668
1306
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
669
1307
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
670
1308
  return c > 3 && r && Object.defineProperty(target, key, r), r;
671
1309
  }
672
- __name(_ts_decorate4, "_ts_decorate");
1310
+ __name(_ts_decorate6, "_ts_decorate");
1311
+ function _ts_metadata5(k, v) {
1312
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1313
+ }
1314
+ __name(_ts_metadata5, "_ts_metadata");
1315
+ function _ts_param5(paramIndex, decorator) {
1316
+ return function(target, key) {
1317
+ decorator(target, key, paramIndex);
1318
+ };
1319
+ }
1320
+ __name(_ts_param5, "_ts_param");
1321
+ var DurableUiSessionGuard = class {
1322
+ static {
1323
+ __name(this, "DurableUiSessionGuard");
1324
+ }
1325
+ auth;
1326
+ basePath;
1327
+ constructor(auth, basePath) {
1328
+ this.auth = auth;
1329
+ this.basePath = basePath;
1330
+ }
1331
+ canActivate(context) {
1332
+ if (!this.auth) return true;
1333
+ const http = context.switchToHttp();
1334
+ const request = http.getRequest();
1335
+ const session = readSessionFromRequest(this.auth, request);
1336
+ if (!session) {
1337
+ const returnTo = encodeURIComponent(originalRequestUrl(request));
1338
+ throw new DashboardLoginRedirectException(`${this.basePath}/login?returnTo=${returnTo}`);
1339
+ }
1340
+ maybeRenewSession(this.auth, session, request, http.getResponse());
1341
+ return true;
1342
+ }
1343
+ };
1344
+ DurableUiSessionGuard = _ts_decorate6([
1345
+ (0, import_common6.Injectable)(),
1346
+ _ts_param5(0, (0, import_common6.Inject)(DASHBOARD_AUTH)),
1347
+ _ts_param5(1, (0, import_common6.Inject)(DASHBOARD_BASE_PATH)),
1348
+ _ts_metadata5("design:type", Function),
1349
+ _ts_metadata5("design:paramtypes", [
1350
+ Object,
1351
+ String
1352
+ ])
1353
+ ], DurableUiSessionGuard);
1354
+ var DurableApiSessionGuard = class {
1355
+ static {
1356
+ __name(this, "DurableApiSessionGuard");
1357
+ }
1358
+ auth;
1359
+ constructor(auth) {
1360
+ this.auth = auth;
1361
+ }
1362
+ canActivate(context) {
1363
+ if (!this.auth) return true;
1364
+ const http = context.switchToHttp();
1365
+ const request = http.getRequest();
1366
+ const session = readSessionFromRequest(this.auth, request);
1367
+ if (!session) throw new import_common6.UnauthorizedException();
1368
+ maybeRenewSession(this.auth, session, request, http.getResponse());
1369
+ return true;
1370
+ }
1371
+ };
1372
+ DurableApiSessionGuard = _ts_decorate6([
1373
+ (0, import_common6.Injectable)(),
1374
+ _ts_param5(0, (0, import_common6.Inject)(DASHBOARD_AUTH)),
1375
+ _ts_metadata5("design:type", Function),
1376
+ _ts_metadata5("design:paramtypes", [
1377
+ Object
1378
+ ])
1379
+ ], DurableApiSessionGuard);
1380
+
1381
+ // src/server/durable-dashboard.module.ts
1382
+ function _ts_decorate7(decorators, target, key, desc) {
1383
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1384
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1385
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1386
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1387
+ }
1388
+ __name(_ts_decorate7, "_ts_decorate");
673
1389
  var GUARDS_METADATA = "__guards__";
674
1390
  function normalize(path) {
675
1391
  return `/${path.replace(/^\/+|\/+$/g, "")}`;
676
1392
  }
677
1393
  __name(normalize, "normalize");
678
- function stampGuards(guards, ...controllers) {
679
- for (const controller of controllers) {
680
- Reflect.defineMetadata(GUARDS_METADATA, guards ?? [], controller);
681
- }
1394
+ function stampGuards(hostGuards, builtIn, controller) {
1395
+ const merged = [
1396
+ ...builtIn ? [
1397
+ builtIn
1398
+ ] : [],
1399
+ ...hostGuards ?? []
1400
+ ];
1401
+ Reflect.defineMetadata(GUARDS_METADATA, merged, controller);
682
1402
  }
683
1403
  __name(stampGuards, "stampGuards");
684
1404
  var DurableApiModule = class _DurableApiModule {
@@ -686,6 +1406,7 @@ var DurableApiModule = class _DurableApiModule {
686
1406
  __name(this, "DurableApiModule");
687
1407
  }
688
1408
  static register(options) {
1409
+ stampGuards(options.guards, options.authMayBeConfigured ? DurableApiSessionGuard : void 0, DurableApiController);
689
1410
  return {
690
1411
  module: _DurableApiModule,
691
1412
  imports: [
@@ -696,6 +1417,8 @@ var DurableApiModule = class _DurableApiModule {
696
1417
  ],
697
1418
  providers: [
698
1419
  DashboardService,
1420
+ options.authProvider,
1421
+ DurableApiSessionGuard,
699
1422
  ...options.guards ?? []
700
1423
  ],
701
1424
  exports: [
@@ -704,17 +1427,33 @@ var DurableApiModule = class _DurableApiModule {
704
1427
  };
705
1428
  }
706
1429
  };
707
- DurableApiModule = _ts_decorate4([
708
- (0, import_common4.Module)({})
1430
+ DurableApiModule = _ts_decorate7([
1431
+ (0, import_common7.Module)({})
709
1432
  ], DurableApiModule);
710
1433
  var DurableDashboardModule = class _DurableDashboardModule {
711
1434
  static {
712
1435
  __name(this, "DurableDashboardModule");
713
1436
  }
714
1437
  static forRoot(options = {}) {
1438
+ const authProvider = {
1439
+ provide: DASHBOARD_AUTH,
1440
+ useValue: resolveDashboardAuth(options.dashboardAuth)
1441
+ };
1442
+ return _DurableDashboardModule.build(options, authProvider, options.dashboardAuth !== void 0);
1443
+ }
1444
+ static forRootAsync(options) {
1445
+ const authProvider = {
1446
+ provide: DASHBOARD_AUTH,
1447
+ inject: options.inject ?? [],
1448
+ useFactory: /* @__PURE__ */ __name(async (...deps) => resolveDashboardAuth(await options.useDashboardAuth(...deps)), "useFactory")
1449
+ };
1450
+ return _DurableDashboardModule.build(options, authProvider, true);
1451
+ }
1452
+ /** Shared wiring: static routing + the API module, with `dashboardAuth` supplied by `authProvider`. */
1453
+ static build(options, authProvider, authMayBeConfigured) {
715
1454
  const basePath = normalize(options.basePath ?? "/durable");
716
1455
  const apiBasePath = normalize(options.apiBasePath ?? `${basePath}/api`);
717
- stampGuards(options.guards, DurableApiController, DurableUiController);
1456
+ stampGuards(options.guards, authMayBeConfigured ? DurableUiSessionGuard : void 0, DurableUiController);
718
1457
  return {
719
1458
  module: _DurableDashboardModule,
720
1459
  imports: [
@@ -728,7 +1467,9 @@ var DurableDashboardModule = class _DurableDashboardModule {
728
1467
  } : {},
729
1468
  ...options.guards ? {
730
1469
  guards: options.guards
731
- } : {}
1470
+ } : {},
1471
+ authProvider,
1472
+ authMayBeConfigured
732
1473
  }),
733
1474
  import_core.RouterModule.register([
734
1475
  {
@@ -742,7 +1483,13 @@ var DurableDashboardModule = class _DurableDashboardModule {
742
1483
  ])
743
1484
  ],
744
1485
  controllers: [
745
- DurableUiController
1486
+ DurableUiController,
1487
+ // The auth controller (login page + logout) only exists when dashboardAuth might be
1488
+ // configured — mounting it unconditionally would add a new `/login` route even for hosts
1489
+ // that never set `dashboardAuth`, breaking "absent -> today's behavior byte-for-byte".
1490
+ ...authMayBeConfigured ? [
1491
+ DurableAuthController
1492
+ ] : []
746
1493
  ],
747
1494
  providers: [
748
1495
  {
@@ -753,7 +1500,10 @@ var DurableDashboardModule = class _DurableDashboardModule {
753
1500
  provide: DASHBOARD_API_PATH,
754
1501
  useValue: apiBasePath
755
1502
  },
756
- // DurableUiController is hosted HERE, so its guards DI-instantiate from this module.
1503
+ authProvider,
1504
+ DurableUiSessionGuard,
1505
+ // DurableUiController (and DurableAuthController, when present) are hosted HERE, so their
1506
+ // guards DI-instantiate from this module.
757
1507
  ...options.guards ?? []
758
1508
  ],
759
1509
  // Re-export the API module so its DashboardService reaches importers (e.g. flip's own controllers).
@@ -763,8 +1513,8 @@ var DurableDashboardModule = class _DurableDashboardModule {
763
1513
  };
764
1514
  }
765
1515
  };
766
- DurableDashboardModule = _ts_decorate4([
767
- (0, import_common4.Module)({})
1516
+ DurableDashboardModule = _ts_decorate7([
1517
+ (0, import_common7.Module)({})
768
1518
  ], DurableDashboardModule);
769
1519
  // Annotate the CommonJS export names for ESM import in node:
770
1520
  0 && (module.exports = {
@@ -773,7 +1523,10 @@ DurableDashboardModule = _ts_decorate4([
773
1523
  DashboardService,
774
1524
  DurableApiController,
775
1525
  DurableApiModule,
1526
+ DurableApiSessionGuard,
1527
+ DurableAuthController,
776
1528
  DurableDashboardModule,
777
- DurableUiController
1529
+ DurableUiController,
1530
+ DurableUiSessionGuard
778
1531
  });
779
1532
  //# sourceMappingURL=index.cjs.map