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