@dudousxd/nestjs-durable-dashboard 0.30.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,14 +557,468 @@ DurableApiController = _ts_decorate2([
554
557
  ])
555
558
  ], DurableApiController);
556
559
 
557
- // src/server/durable-dashboard.module.ts
558
- var import_common4 = require("@nestjs/common");
559
- 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");
560
1014
 
561
1015
  // src/server/durable-ui.controller.ts
562
1016
  var import_node_fs = require("fs");
563
1017
  var import_node_path = require("path");
564
1018
  var import_node_url = require("url");
1019
+ var import_common4 = require("@nestjs/common");
1020
+
1021
+ // src/server/auth/login-redirect.exception.ts
565
1022
  var import_common3 = require("@nestjs/common");
566
1023
  function _ts_decorate3(decorators, target, key, desc) {
567
1024
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -570,6 +1027,38 @@ function _ts_decorate3(decorators, target, key, desc) {
570
1027
  return c > 3 && r && Object.defineProperty(target, key, r), r;
571
1028
  }
572
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");
573
1062
  function _ts_metadata3(k, v) {
574
1063
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
575
1064
  }
@@ -612,7 +1101,7 @@ var DurableUiController = class {
612
1101
  index() {
613
1102
  const indexPath = (0, import_node_path.join)(this.dir, "index.html");
614
1103
  if (!(0, import_node_fs.existsSync)(indexPath)) {
615
- 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.");
616
1105
  }
617
1106
  const html = (0, import_node_fs.readFileSync)(indexPath, "utf8").replaceAll(`="${BUILD_BASE}/`, `="${this.basePath}/`);
618
1107
  const inject = `<script>window.__DURABLE_BASE__='${this.basePath}';window.__DURABLE_API__='${this.apiBasePath}';</script>`;
@@ -620,40 +1109,41 @@ var DurableUiController = class {
620
1109
  }
621
1110
  asset(file) {
622
1111
  const safe = (0, import_node_path.basename)(file);
623
- if (safe !== file) throw new import_common3.NotFoundException();
1112
+ if (safe !== file) throw new import_common4.NotFoundException();
624
1113
  const root = (0, import_node_path.resolve)(this.dir, "assets");
625
1114
  const assetPath = (0, import_node_path.resolve)(root, safe);
626
1115
  if (!assetPath.startsWith(root + import_node_path.sep) || !(0, import_node_fs.existsSync)(assetPath)) {
627
- throw new import_common3.NotFoundException();
1116
+ throw new import_common4.NotFoundException();
628
1117
  }
629
1118
  const type = CONTENT_TYPES[(0, import_node_path.extname)(safe)] ?? "application/octet-stream";
630
- return new import_common3.StreamableFile((0, import_node_fs.readFileSync)(assetPath), {
1119
+ return new import_common4.StreamableFile((0, import_node_fs.readFileSync)(assetPath), {
631
1120
  type
632
1121
  });
633
1122
  }
634
1123
  };
635
- _ts_decorate3([
636
- (0, import_common3.Get)(),
637
- (0, import_common3.Header)("Content-Type", "text/html; charset=utf-8"),
638
- (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"),
639
1128
  _ts_metadata3("design:type", Function),
640
1129
  _ts_metadata3("design:paramtypes", []),
641
1130
  _ts_metadata3("design:returntype", String)
642
1131
  ], DurableUiController.prototype, "index", null);
643
- _ts_decorate3([
644
- (0, import_common3.Get)("assets/:file"),
645
- (0, import_common3.Header)("Cache-Control", "public, max-age=31536000, immutable"),
646
- _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")),
647
1136
  _ts_metadata3("design:type", Function),
648
1137
  _ts_metadata3("design:paramtypes", [
649
1138
  String
650
1139
  ]),
651
- _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)
652
1141
  ], DurableUiController.prototype, "asset", null);
653
- DurableUiController = _ts_decorate3([
654
- (0, import_common3.Controller)(),
655
- _ts_param3(0, (0, import_common3.Inject)(DASHBOARD_BASE_PATH)),
656
- _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)),
657
1147
  _ts_metadata3("design:type", Function),
658
1148
  _ts_metadata3("design:paramtypes", [
659
1149
  String,
@@ -661,47 +1151,326 @@ DurableUiController = _ts_decorate3([
661
1151
  ])
662
1152
  ], DurableUiController);
663
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
+
664
1297
  // src/server/durable-dashboard.module.ts
665
- 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) {
666
1305
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
667
1306
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
668
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;
669
1308
  return c > 3 && r && Object.defineProperty(target, key, r), r;
670
1309
  }
671
- __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");
1389
+ var GUARDS_METADATA = "__guards__";
672
1390
  function normalize(path) {
673
1391
  return `/${path.replace(/^\/+|\/+$/g, "")}`;
674
1392
  }
675
1393
  __name(normalize, "normalize");
676
- var DurableApiModule = class {
1394
+ function stampGuards(hostGuards, builtIn, controller) {
1395
+ const merged = [
1396
+ ...builtIn ? [
1397
+ builtIn
1398
+ ] : [],
1399
+ ...hostGuards ?? []
1400
+ ];
1401
+ Reflect.defineMetadata(GUARDS_METADATA, merged, controller);
1402
+ }
1403
+ __name(stampGuards, "stampGuards");
1404
+ var DurableApiModule = class _DurableApiModule {
677
1405
  static {
678
1406
  __name(this, "DurableApiModule");
679
1407
  }
1408
+ static register(options) {
1409
+ stampGuards(options.guards, options.authMayBeConfigured ? DurableApiSessionGuard : void 0, DurableApiController);
1410
+ return {
1411
+ module: _DurableApiModule,
1412
+ imports: [
1413
+ ...options.imports ?? []
1414
+ ],
1415
+ controllers: [
1416
+ DurableApiController
1417
+ ],
1418
+ providers: [
1419
+ DashboardService,
1420
+ options.authProvider,
1421
+ DurableApiSessionGuard,
1422
+ ...options.guards ?? []
1423
+ ],
1424
+ exports: [
1425
+ DashboardService
1426
+ ]
1427
+ };
1428
+ }
680
1429
  };
681
- DurableApiModule = _ts_decorate4([
682
- (0, import_common4.Module)({
683
- controllers: [
684
- DurableApiController
685
- ],
686
- providers: [
687
- DashboardService
688
- ],
689
- exports: [
690
- DashboardService
691
- ]
692
- })
1430
+ DurableApiModule = _ts_decorate7([
1431
+ (0, import_common7.Module)({})
693
1432
  ], DurableApiModule);
694
1433
  var DurableDashboardModule = class _DurableDashboardModule {
695
1434
  static {
696
1435
  __name(this, "DurableDashboardModule");
697
1436
  }
698
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) {
699
1454
  const basePath = normalize(options.basePath ?? "/durable");
700
1455
  const apiBasePath = normalize(options.apiBasePath ?? `${basePath}/api`);
1456
+ stampGuards(options.guards, authMayBeConfigured ? DurableUiSessionGuard : void 0, DurableUiController);
701
1457
  return {
702
1458
  module: _DurableDashboardModule,
703
1459
  imports: [
704
- DurableApiModule,
1460
+ ...options.imports ?? [],
1461
+ // Guards + host imports must reach the API controller's HOST module — enhancers resolve
1462
+ // from their controller's own module, never from a parent (see DurableApiModule.register).
1463
+ // Spread-only-when-set: exactOptionalPropertyTypes rejects an explicit `undefined`.
1464
+ DurableApiModule.register({
1465
+ ...options.imports ? {
1466
+ imports: options.imports
1467
+ } : {},
1468
+ ...options.guards ? {
1469
+ guards: options.guards
1470
+ } : {},
1471
+ authProvider,
1472
+ authMayBeConfigured
1473
+ }),
705
1474
  import_core.RouterModule.register([
706
1475
  {
707
1476
  path: basePath,
@@ -714,7 +1483,13 @@ var DurableDashboardModule = class _DurableDashboardModule {
714
1483
  ])
715
1484
  ],
716
1485
  controllers: [
717
- 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
+ ] : []
718
1493
  ],
719
1494
  providers: [
720
1495
  {
@@ -724,7 +1499,12 @@ var DurableDashboardModule = class _DurableDashboardModule {
724
1499
  {
725
1500
  provide: DASHBOARD_API_PATH,
726
1501
  useValue: apiBasePath
727
- }
1502
+ },
1503
+ authProvider,
1504
+ DurableUiSessionGuard,
1505
+ // DurableUiController (and DurableAuthController, when present) are hosted HERE, so their
1506
+ // guards DI-instantiate from this module.
1507
+ ...options.guards ?? []
728
1508
  ],
729
1509
  // Re-export the API module so its DashboardService reaches importers (e.g. flip's own controllers).
730
1510
  exports: [
@@ -733,8 +1513,8 @@ var DurableDashboardModule = class _DurableDashboardModule {
733
1513
  };
734
1514
  }
735
1515
  };
736
- DurableDashboardModule = _ts_decorate4([
737
- (0, import_common4.Module)({})
1516
+ DurableDashboardModule = _ts_decorate7([
1517
+ (0, import_common7.Module)({})
738
1518
  ], DurableDashboardModule);
739
1519
  // Annotate the CommonJS export names for ESM import in node:
740
1520
  0 && (module.exports = {
@@ -743,7 +1523,10 @@ DurableDashboardModule = _ts_decorate4([
743
1523
  DashboardService,
744
1524
  DurableApiController,
745
1525
  DurableApiModule,
1526
+ DurableApiSessionGuard,
1527
+ DurableAuthController,
746
1528
  DurableDashboardModule,
747
- DurableUiController
1529
+ DurableUiController,
1530
+ DurableUiSessionGuard
748
1531
  });
749
1532
  //# sourceMappingURL=index.cjs.map