@dudousxd/nestjs-durable-dashboard 0.31.0 → 0.32.1

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