@webtypen/webframez-core 0.3.73 → 0.3.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/auth-client.js CHANGED
@@ -8,6 +8,7 @@ function csrfCookie(name) {
8
8
  exports.createAuthFetch = function createAuthFetch(options = {}, originalFetch = window.fetch.bind(window)) {
9
9
  const base = (options.basePath || "/api/auth").replace(/\/+$/, "");
10
10
  const loginPaths = options.loginPaths || [`${base}/login`];
11
+ let refreshing;
11
12
  return async (input, init) => {
12
13
  const url = new URL(input instanceof Request ? input.url : String(input), window.location.href);
13
14
  const method = String(init?.method || (input instanceof Request ? input.method : "GET")).toUpperCase();
@@ -21,7 +22,23 @@ exports.createAuthFetch = function createAuthFetch(options = {}, originalFetch =
21
22
  } else token = csrfCookie(options.cookieName);
22
23
  const headers = new Headers(init?.headers || (input instanceof Request ? input.headers : undefined));
23
24
  if (typeof token === "string" && token) headers.set("X-CSRF-Token", token);
24
- return originalFetch(input, { ...init, headers });
25
+ const retryInput = input instanceof Request ? input.clone() : input;
26
+ const response = await originalFetch(input, { ...init, headers });
27
+ if (loginPaths.includes(url.pathname) || url.pathname.startsWith(`${base}/`)
28
+ || ![401, 403].includes(response.status)) return response;
29
+ const failure = await response.clone().json().catch(() => null);
30
+ if (!["unauthorized", "invalid_csrf"].includes(failure?.code)) return response;
31
+ if (!refreshing) {
32
+ const refreshHeaders = new Headers();
33
+ const csrf = csrfCookie(options.cookieName);
34
+ if (csrf) refreshHeaders.set("X-CSRF-Token", csrf);
35
+ refreshing = originalFetch(`${base}/refresh`, { method: "POST", credentials: "same-origin", headers: refreshHeaders })
36
+ .then(result => result.ok, () => false).finally(() => { refreshing = undefined; });
37
+ }
38
+ if (!await refreshing) return response;
39
+ const nextCsrf = csrfCookie(options.cookieName);
40
+ if (nextCsrf) headers.set("X-CSRF-Token", nextCsrf);
41
+ return originalFetch(retryInput, { ...init, headers });
25
42
  };
26
43
  };
27
44
 
@@ -0,0 +1,21 @@
1
+ import { Model } from "../Database/Model";
2
+ import type { Request } from "../Router/Request";
3
+ import type { Response } from "../Router/Response";
4
+ import { AuthScope, AuthScopeOptions, AuthOperation, AuthRouteOptions } from "./ModelAuth";
5
+ export type AuthConfig = {
6
+ /** Disable automatic main creation; explicitly configured scopes remain available. */
7
+ main?: boolean;
8
+ defaults?: Partial<AuthScopeOptions>;
9
+ scopes?: Record<string, AuthScopeOptions>;
10
+ };
11
+ /** Application-wide registry of named browser-authentication scopes. */
12
+ export declare class AuthFacade {
13
+ private readonly scopes;
14
+ private initialized;
15
+ init(): void;
16
+ scope(key?: string): AuthScope;
17
+ revokeUserSessions(model: typeof Model, subject: string): Promise<void>;
18
+ registerScope(key: string, config: Partial<AuthScopeOptions>): AuthScope;
19
+ }
20
+ export declare const Auth: AuthFacade;
21
+ export declare function authRoute(operation: AuthOperation, options: AuthRouteOptions): (req: Request, res: Response) => Promise<Response>;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.authRoute = exports.Auth = exports.AuthFacade = void 0;
13
+ const Config_1 = require("../Config");
14
+ const Model_1 = require("../Database/Model");
15
+ const ModelAuth_1 = require("./ModelAuth");
16
+ function mergeOptions(base, config) {
17
+ return Object.assign(Object.assign(Object.assign({ model: Model_1.Model }, base), config), { fields: Object.assign(Object.assign({}, base.fields), config.fields), session: Object.assign(Object.assign({}, base.session), config.session), passwordReset: config.passwordReset === false ? false : config.passwordReset === undefined && base.passwordReset === false ? false
18
+ : Object.assign(Object.assign({}, (base.passwordReset || {})), (config.passwordReset || {})), messages: Object.assign(Object.assign({}, base.messages), config.messages) });
19
+ }
20
+ /** Application-wide registry of named browser-authentication scopes. */
21
+ class AuthFacade {
22
+ constructor() {
23
+ this.scopes = new Map();
24
+ this.initialized = false;
25
+ }
26
+ init() {
27
+ if (this.initialized)
28
+ return;
29
+ const config = Config_1.Config.get("auth") || {};
30
+ const configured = new Map();
31
+ for (const [key, options] of Object.entries(config.scopes || {})) {
32
+ configured.set(key, new ModelAuth_1.AuthScope(Object.assign(Object.assign({}, mergeOptions(config.defaults || {}, options)), { key })));
33
+ }
34
+ if (config.main !== false && !configured.has("main")) {
35
+ configured.set("main", new ModelAuth_1.AuthScope(Object.assign(Object.assign({}, mergeOptions(config.defaults || {}, {})), { key: "main" })));
36
+ }
37
+ for (const [key, scope] of configured)
38
+ this.scopes.set(key, scope);
39
+ this.initialized = true;
40
+ }
41
+ scope(key = "main") {
42
+ this.init();
43
+ const scope = this.scopes.get(key);
44
+ if (!scope)
45
+ throw new Error(`Auth scope "${key}" is not registered.`);
46
+ return scope;
47
+ }
48
+ revokeUserSessions(model, subject) {
49
+ return __awaiter(this, void 0, void 0, function* () {
50
+ this.init();
51
+ for (const scope of this.scopes.values()) {
52
+ if (scope.configuration.model === model)
53
+ yield scope.sessions.revokeAll(subject);
54
+ }
55
+ });
56
+ }
57
+ registerScope(key, config) {
58
+ var _a;
59
+ this.init();
60
+ const defaults = Config_1.Config.get("auth.defaults") || {};
61
+ const base = ((_a = this.scopes.get(key)) === null || _a === void 0 ? void 0 : _a.configuration) || defaults;
62
+ const scope = new ModelAuth_1.AuthScope(Object.assign(Object.assign({}, mergeOptions(base, config)), { key }));
63
+ this.scopes.set(key, scope);
64
+ return scope;
65
+ }
66
+ }
67
+ exports.AuthFacade = AuthFacade;
68
+ exports.Auth = new AuthFacade();
69
+ function authRoute(operation, options) {
70
+ return (req, res) => exports.Auth.scope(options.auth || "main").handle(operation, req, res, options.loginResponse);
71
+ }
72
+ exports.authRoute = authRoute;
@@ -1,6 +1,7 @@
1
1
  import { Model } from "../Database/Model";
2
2
  import { Request } from "../Router/Request";
3
3
  import type { Response } from "../Router/Response";
4
+ import { PasswordReset, PasswordResetOptions } from "./PasswordReset";
4
5
  import { AuthSession, SessionAuthOptions } from "./SessionAuth";
5
6
  import { WebAuth, WebAuthError, WebAuthOptions } from "./WebAuth";
6
7
  export type AuthContext<TUser extends Model = Model> = {
@@ -11,18 +12,20 @@ export type AuthSnapshot<TUser = Record<string, unknown>> = {
11
12
  user: TUser;
12
13
  session: AuthSession;
13
14
  };
14
- export type AuthErrorCode = "missing_login" | "invalid_login" | "inactive_user" | "invalid_origin" | "invalid_csrf" | "unauthorized" | "server_error";
15
+ export type AuthErrorCode = "missing_login" | "invalid_login" | "inactive_user" | "invalid_origin" | "invalid_csrf" | "unauthorized" | "server_error" | "login_blocked" | "logout_blocked";
15
16
  export declare class AuthLoginError extends WebAuthError {
16
17
  readonly code: AuthErrorCode;
17
18
  constructor(code: AuthErrorCode, status: 401 | 403, message: string);
18
19
  }
19
20
  export type ModelAuthOptions = Partial<WebAuthOptions> & {
20
21
  model: typeof Model;
22
+ key?: string;
21
23
  /** Takes precedence over session.audience; defaults to the configured origin. */
22
24
  audience?: string;
23
25
  locale?: "en" | "de";
24
26
  /** Record browser metadata and update activity at most once per minute. */
25
27
  trackActivity?: boolean;
28
+ passwordReset?: PasswordResetOptions | false;
26
29
  fields?: {
27
30
  identifier?: string;
28
31
  password?: string;
@@ -33,33 +36,49 @@ export type ModelAuthOptions = Partial<WebAuthOptions> & {
33
36
  csrfDisabled?: false;
34
37
  verifyPassword?: (password: string, storedHash: string) => boolean | Promise<boolean>;
35
38
  isUserAllowed?: (user: Model) => boolean | Promise<boolean>;
39
+ beforeLogin?: AuthActionHook;
40
+ afterLogin?: AuthActionHook;
41
+ beforeLogout?: AuthActionHook;
42
+ afterLogout?: AuthActionHook;
36
43
  onLogin?: (session: AuthSession, req: Request) => void | Promise<void>;
37
44
  publicUserFields?: readonly string[];
38
45
  onAuthenticate?: (auth: AuthContext, req: Request) => void | Promise<void>;
39
46
  messages?: Partial<Record<AuthErrorCode, string>>;
40
47
  };
48
+ export type AuthActionHook = (req: Request, user: Model, scope: AuthScope) => void | Promise<void>;
41
49
  export type AuthOperation = "csrf" | "login" | "logout" | "refresh";
42
50
  export type AuthRouteOptions = Partial<ModelAuthOptions> & {
43
- auth?: ModelAuth | (() => ModelAuth);
51
+ auth?: string;
44
52
  loginPath?: string;
45
53
  loginResponse?: (req: Request, session: AuthSession) => object | Promise<object>;
46
54
  middleware?: string[];
47
55
  domains?: string[];
48
56
  };
49
57
  /** Model-backed browser authentication with secure default routes and revocable sessions. */
50
- export declare class ModelAuth extends WebAuth {
58
+ export declare class AuthScope extends WebAuth {
51
59
  private readonly modelOptions;
60
+ readonly key: string;
61
+ readonly passwordReset: PasswordReset | null;
52
62
  private readonly model;
63
+ private authPaths;
53
64
  private readonly sessionUsers;
54
65
  private readonly fields;
55
66
  constructor(modelOptions: ModelAuthOptions);
67
+ configureRoutes(basePath: string, loginPath?: string): void;
68
+ get browserConfiguration(): {
69
+ basePath: string;
70
+ loginPaths: string[];
71
+ cookieName: string;
72
+ };
73
+ get configuration(): ModelAuthOptions;
56
74
  private message;
57
75
  /** Use this when creating users; existing bcrypt hashes are supported without migration. */
58
76
  static hashPassword(password: string): Promise<string>;
59
77
  verifyCredentials(req: Request): Promise<string>;
60
78
  login(req: Request, res: Response, verifyCredentials?: () => Promise<string | null>): Promise<AuthSession>;
79
+ logout(req: Request, res: Response): Promise<void>;
61
80
  /** Resolve a fresh validated session and its model; never trusts a client-supplied req.auth. */
62
- resolve<TUser extends Model = Model>(req: Request): Promise<AuthContext<TUser> | null>;
81
+ resolve<TUser extends Model = Model>(req: Request, res?: Response): Promise<AuthContext<TUser> | null>;
63
82
  resolveCookies<TUser extends Model = Model>(cookies: Record<string, string | undefined>): Promise<AuthContext<TUser> | null>;
64
83
  /** Only explicit public fields are sent to browsers; Model instances remain on the server. */
65
84
  snapshot(auth: AuthContext | null | undefined): AuthSnapshot | null;
@@ -69,4 +88,6 @@ export declare class ModelAuth extends WebAuth {
69
88
  }): (next: Function, reject: Function, req: Request, res: Response) => Promise<any>;
70
89
  handle(operation: AuthOperation, req: Request, res: Response, loginResponse?: AuthRouteOptions["loginResponse"]): Promise<Response>;
71
90
  }
72
- export declare function authRoute(operation: AuthOperation, options: AuthRouteOptions): (req: Request, res: Response) => Promise<Response>;
91
+ /** Compatibility name for existing integrations. Prefer Auth.scope(). */
92
+ export { AuthScope as ModelAuth };
93
+ export type AuthScopeOptions = ModelAuthOptions;
@@ -9,10 +9,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.authRoute = exports.ModelAuth = exports.AuthLoginError = void 0;
12
+ exports.ModelAuth = exports.AuthScope = exports.AuthLoginError = void 0;
13
13
  const bcryptjs_1 = require("bcryptjs");
14
+ const routing_1 = require("../routing");
14
15
  const Config_1 = require("../Config");
15
16
  const Request_1 = require("../Router/Request");
17
+ const Auth_1 = require("./Auth");
18
+ const PasswordReset_1 = require("./PasswordReset");
16
19
  const AuthSecurity_1 = require("./AuthSecurity");
17
20
  const SessionAuth_1 = require("./SessionAuth");
18
21
  const WebAuth_1 = require("./WebAuth");
@@ -23,6 +26,11 @@ class AuthLoginError extends WebAuth_1.WebAuthError {
23
26
  }
24
27
  }
25
28
  exports.AuthLoginError = AuthLoginError;
29
+ class AuthHookError extends AuthLoginError {
30
+ constructor(code, error) {
31
+ super(code, error instanceof WebAuth_1.WebAuthError ? error.status : 403, error instanceof Error ? error.message : typeof error === "string" ? error : "Authentication action was blocked.");
32
+ }
33
+ }
26
34
  const defaultMessages = {
27
35
  en: {
28
36
  missing_login: "Please provide your login and password.",
@@ -32,6 +40,8 @@ const defaultMessages = {
32
40
  invalid_csrf: "Please reload the page and try logging in again.",
33
41
  unauthorized: "Please log in.",
34
42
  server_error: "Authentication could not be processed.",
43
+ login_blocked: "Login was blocked.",
44
+ logout_blocked: "Logout was blocked.",
35
45
  },
36
46
  de: {
37
47
  missing_login: "Bitte gib E-Mail-Adresse und Passwort an.",
@@ -41,19 +51,24 @@ const defaultMessages = {
41
51
  invalid_csrf: "Der Login konnte gerade nicht verarbeitet werden. Bitte lade die Seite neu.",
42
52
  unauthorized: "Bitte melde dich an.",
43
53
  server_error: "Die Anmeldung konnte gerade nicht verarbeitet werden.",
54
+ login_blocked: "Die Anmeldung wurde abgelehnt.",
55
+ logout_blocked: "Die Abmeldung wurde abgelehnt.",
44
56
  },
45
57
  };
46
58
  // Verify a dummy password hash for unknown accounts as well.
47
59
  let dummyHash;
48
60
  /** Model-backed browser authentication with secure default routes and revocable sessions. */
49
- class ModelAuth extends WebAuth_1.WebAuth {
61
+ class AuthScope extends WebAuth_1.WebAuth {
50
62
  constructor(modelOptions) {
51
- var _a, _b, _c, _d, _e;
63
+ var _a, _b, _c, _d, _e, _f, _g, _h;
52
64
  if (modelOptions.csrfDisabled)
53
65
  throw new Error("Browser auth routes require CSRF protection.");
54
66
  if (!modelOptions.model)
55
67
  throw new Error("Route.auth requires a Model class.");
56
68
  const origin = new URL(modelOptions.origin || Config_1.Config.get("auth.origin") || Config_1.Config.get("website.baseUrl") || Config_1.Config.get("application.website.baseUrl") || process.env.WEBSITE_URL || process.env.PUBLIC_BASE_URL || `http://localhost:${process.env.PORT || 3035}`).origin;
69
+ const key = (_a = modelOptions.key) !== null && _a !== void 0 ? _a : "main";
70
+ if (!/^[A-Za-z0-9_-]+$/.test(key))
71
+ throw new Error("Invalid auth scope key.");
57
72
  const model = modelOptions.model;
58
73
  const fields = Object.assign({ identifier: "email", password: "password", active: "is_active", primaryKey: new model().__primaryKey || "_id" }, modelOptions.fields);
59
74
  const allowed = (user) => __awaiter(this, void 0, void 0, function* () {
@@ -61,7 +76,7 @@ class ModelAuth extends WebAuth_1.WebAuth {
61
76
  && (!modelOptions.isUserAllowed || (yield modelOptions.isUserAllowed(user))));
62
77
  });
63
78
  const sessionUsers = new WeakMap();
64
- const sessions = new SessionAuth_1.SessionAuth(Object.assign(Object.assign({ accessTokenSeconds: 30 * 86400, sessionSeconds: 30 * 86400 }, modelOptions.session), { issuer: origin, audience: (_c = (_a = modelOptions.audience) !== null && _a !== void 0 ? _a : (_b = modelOptions.session) === null || _b === void 0 ? void 0 : _b.audience) !== null && _c !== void 0 ? _c : origin, connection: ((_d = modelOptions.session) === null || _d === void 0 ? void 0 : _d.connection) || new model().__connection, isSessionAllowed: (session) => __awaiter(this, void 0, void 0, function* () {
79
+ const sessions = new SessionAuth_1.SessionAuth(Object.assign(Object.assign({ accessTokenSeconds: 15 * 60, sessionSeconds: 30 * 86400, idleTimeoutSeconds: 7 * 86400 }, modelOptions.session), { issuer: origin, scope: key, audience: (_d = (_b = modelOptions.audience) !== null && _b !== void 0 ? _b : (_c = modelOptions.session) === null || _c === void 0 ? void 0 : _c.audience) !== null && _d !== void 0 ? _d : origin, connection: ((_e = modelOptions.session) === null || _e === void 0 ? void 0 : _e.connection) || new model().__connection, isSessionAllowed: (session) => __awaiter(this, void 0, void 0, function* () {
65
80
  const id = fields.primaryKey === "_id" ? yield model.objectId(session.subject, { noExceptions: true }) : session.subject;
66
81
  const user = id !== null && id !== undefined ? yield model.where(fields.primaryKey, "=", id).first() : null;
67
82
  if (!(yield allowed(user)))
@@ -69,11 +84,28 @@ class ModelAuth extends WebAuth_1.WebAuth {
69
84
  sessionUsers.set(session, user);
70
85
  return true;
71
86
  }) }));
72
- super(sessions, Object.assign(Object.assign({}, modelOptions), { origin, allowInsecureLocalhost: (_e = modelOptions.allowInsecureLocalhost) !== null && _e !== void 0 ? _e : true }));
87
+ super(sessions, Object.assign(Object.assign({}, modelOptions), { origin, cookiePrefix: (_f = modelOptions.cookiePrefix) !== null && _f !== void 0 ? _f : (key === "main" ? undefined : `${origin.startsWith("https:") ? "__Host-wf_" : "wf_dev_"}${key}_`), allowInsecureLocalhost: (_g = modelOptions.allowInsecureLocalhost) !== null && _g !== void 0 ? _g : true }));
73
88
  this.modelOptions = modelOptions;
89
+ this.authPaths = { basePath: "/api/auth", loginPath: "/api/auth/login" };
90
+ this.key = key;
74
91
  this.sessionUsers = sessionUsers;
75
92
  this.model = model;
76
93
  this.fields = fields;
94
+ this.passwordReset = modelOptions.passwordReset === false ? null : new PasswordReset_1.PasswordReset(model, key, fields, modelOptions.passwordReset || {}, (user) => __awaiter(this, void 0, void 0, function* () { return allowed(user); }), (user, mode) => __awaiter(this, void 0, void 0, function* () {
95
+ const subject = String(user[fields.primaryKey]);
96
+ yield this.sessions.revokeAll(subject);
97
+ if (mode === "all")
98
+ yield Auth_1.Auth.revokeUserSessions(model, subject);
99
+ }), (_h = modelOptions.session) === null || _h === void 0 ? void 0 : _h.database);
100
+ }
101
+ configureRoutes(basePath, loginPath) {
102
+ this.authPaths = { basePath, loginPath: loginPath || `${basePath}/login` };
103
+ }
104
+ get browserConfiguration() {
105
+ return { basePath: (0, routing_1.appPath)(this.authPaths.basePath), loginPaths: [(0, routing_1.appPath)(this.authPaths.loginPath)], cookieName: this.cookies.prefix + "csrf" };
106
+ }
107
+ get configuration() {
108
+ return this.modelOptions;
77
109
  }
78
110
  message(code) {
79
111
  var _a, _b;
@@ -112,22 +144,63 @@ class ModelAuth extends WebAuth_1.WebAuth {
112
144
  login: { get: () => super.login }
113
145
  });
114
146
  return __awaiter(this, void 0, void 0, function* () {
115
- const session = yield _super.login.call(this, req, res, verifyCredentials);
147
+ let user = null;
148
+ const session = yield _super.login.call(this, req, res, () => __awaiter(this, void 0, void 0, function* () {
149
+ const subject = yield verifyCredentials();
150
+ if (subject && (this.modelOptions.beforeLogin || this.modelOptions.afterLogin)) {
151
+ const id = this.fields.primaryKey === "_id" ? yield this.model.objectId(subject, { noExceptions: true }) : subject;
152
+ user = id == null ? null : yield this.model.where(this.fields.primaryKey, "=", id).first();
153
+ if (!user)
154
+ throw new AuthLoginError("invalid_login", 401, this.message("invalid_login"));
155
+ if (this.modelOptions.beforeLogin) {
156
+ try {
157
+ yield this.modelOptions.beforeLogin(req, user, this);
158
+ }
159
+ catch (error) {
160
+ throw new AuthHookError("login_blocked", error);
161
+ }
162
+ }
163
+ }
164
+ return subject;
165
+ }));
116
166
  if (this.modelOptions.trackActivity !== false)
117
167
  yield this.sessions.recordLogin(session, (0, AuthSecurity_1.authHeader)(req, "user-agent"));
118
168
  if (this.modelOptions.onLogin)
119
169
  yield this.modelOptions.onLogin(session, req);
170
+ if (this.modelOptions.afterLogin)
171
+ yield this.modelOptions.afterLogin(req, user, this);
120
172
  return session;
121
173
  });
122
174
  }
175
+ logout(req, res) {
176
+ return __awaiter(this, void 0, void 0, function* () {
177
+ const session = yield this.logoutSession(req);
178
+ const user = this.sessionUsers.get(session);
179
+ if (!user)
180
+ throw new AuthLoginError("unauthorized", 401, this.message("unauthorized"));
181
+ if (this.modelOptions.beforeLogout) {
182
+ try {
183
+ yield this.modelOptions.beforeLogout(req, user, this);
184
+ }
185
+ catch (error) {
186
+ throw new AuthHookError("logout_blocked", error);
187
+ }
188
+ }
189
+ yield this.finishLogout(session, res);
190
+ if (this.modelOptions.afterLogout)
191
+ yield this.modelOptions.afterLogout(req, user, this);
192
+ });
193
+ }
123
194
  /** Resolve a fresh validated session and its model; never trusts a client-supplied req.auth. */
124
- resolve(req) {
195
+ resolve(req, res) {
125
196
  const _super = Object.create(null, {
126
- authenticate: { get: () => super.authenticate }
197
+ authenticate: { get: () => super.authenticate },
198
+ resume: { get: () => super.resume }
127
199
  });
128
200
  return __awaiter(this, void 0, void 0, function* () {
201
+ res === null || res === void 0 ? void 0 : res.header("Cache-Control", "no-store");
129
202
  Object.defineProperty(req, "auth", { value: null, writable: true, configurable: true, enumerable: false });
130
- const session = yield _super.authenticate.call(this, req);
203
+ const session = (yield _super.authenticate.call(this, req)) || (res ? yield _super.resume.call(this, req, res) : null);
131
204
  if (!session)
132
205
  return null;
133
206
  const user = this.sessionUsers.get(session);
@@ -157,7 +230,7 @@ class ModelAuth extends WebAuth_1.WebAuth {
157
230
  const user = {};
158
231
  for (const field of fields) {
159
232
  if (field.startsWith("__") || field === this.fields.password || ((_a = auth.user.__hidden) === null || _a === void 0 ? void 0 : _a.includes(field))
160
- || ["constructor", "prototype"].includes(field))
233
+ || field.startsWith("auth_reset_") || ["constructor", "prototype"].includes(field))
161
234
  continue;
162
235
  const value = auth.user[field];
163
236
  if (value !== undefined)
@@ -169,7 +242,7 @@ class ModelAuth extends WebAuth_1.WebAuth {
169
242
  middleware(options = {}) {
170
243
  return (next, reject, req, res) => __awaiter(this, void 0, void 0, function* () {
171
244
  try {
172
- const auth = yield this.resolve(req);
245
+ const auth = yield this.resolve(req, res);
173
246
  res.header("Cache-Control", "no-store");
174
247
  if (!auth && options.required) {
175
248
  res.status(401).send({ status: "error", code: "unauthorized" });
@@ -188,6 +261,7 @@ class ModelAuth extends WebAuth_1.WebAuth {
188
261
  });
189
262
  }
190
263
  handle(operation, req, res, loginResponse) {
264
+ var _a, _b;
191
265
  return __awaiter(this, void 0, void 0, function* () {
192
266
  res.header("Cache-Control", "no-store");
193
267
  try {
@@ -210,17 +284,10 @@ class ModelAuth extends WebAuth_1.WebAuth {
210
284
  if (!(error instanceof WebAuth_1.WebAuthError))
211
285
  console.error(error);
212
286
  return res.status(error instanceof WebAuth_1.WebAuthError ? error.status : 500).send({ status: "error", code,
213
- message: this.message(code) });
287
+ message: error instanceof AuthHookError ? (_b = (_a = this.modelOptions.messages) === null || _a === void 0 ? void 0 : _a[code]) !== null && _b !== void 0 ? _b : error.message : this.message(code) });
214
288
  }
215
289
  });
216
290
  }
217
291
  }
218
- exports.ModelAuth = ModelAuth;
219
- function authRoute(operation, options) {
220
- let auth;
221
- return (req, res) => {
222
- auth || (auth = typeof options.auth === "function" ? options.auth() : options.auth || new ModelAuth(options));
223
- return auth.handle(operation, req, res, options.loginResponse);
224
- };
225
- }
226
- exports.authRoute = authRoute;
292
+ exports.AuthScope = AuthScope;
293
+ exports.ModelAuth = AuthScope;
@@ -0,0 +1,40 @@
1
+ import type { DocumentDatabase } from "../Database/DatabaseAdapter";
2
+ import { Model } from "../Database/Model";
3
+ export type PasswordResetOptions = {
4
+ tokenSeconds?: number;
5
+ cooldownSeconds?: number;
6
+ /** Revoke registered scopes using the same model, only this scope, or leave sessions intact. */
7
+ revokeSessions?: "all" | "scope" | false;
8
+ };
9
+ export type PasswordResetDelivery = (user: Model, token: string, expiresAt: number) => void | Promise<void>;
10
+ /** Framework adapter: the password change and token consumption are one conditional database update. */
11
+ export declare class PasswordReset {
12
+ private readonly model;
13
+ private readonly key;
14
+ private readonly fields;
15
+ private readonly options;
16
+ private readonly allowed;
17
+ private readonly invalidate;
18
+ private readonly database?;
19
+ readonly tokenSeconds: number;
20
+ readonly cooldownSeconds: number;
21
+ readonly revokeSessions: "all" | "scope" | false;
22
+ readonly hiddenFields: string[];
23
+ private readonly hashField;
24
+ private readonly expiresField;
25
+ private readonly requestedField;
26
+ constructor(model: typeof Model, key: string, fields: {
27
+ identifier: string;
28
+ password: string;
29
+ primaryKey: string;
30
+ active: string | false;
31
+ }, options: PasswordResetOptions, allowed: (user: Model) => boolean | Promise<boolean>, invalidate: (user: Model, mode: "all" | "scope") => Promise<void>, database?: (() => Promise<DocumentDatabase>) | undefined);
32
+ private rows;
33
+ private digest;
34
+ private userForToken;
35
+ /** No account-existence result is returned. Deliver the raw token only through a trusted server callback. */
36
+ request(identifier: string, deliver: PasswordResetDelivery): Promise<void>;
37
+ /** A non-consuming check for the reset form; reset() always checks again atomically. */
38
+ validate(token: string): Promise<Model | null>;
39
+ reset(token: string, password: string): Promise<Model | null>;
40
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.PasswordReset = void 0;
13
+ const bcryptjs_1 = require("bcryptjs");
14
+ const DBConnection_1 = require("../Database/DBConnection");
15
+ const AuthSecurity_1 = require("./AuthSecurity");
16
+ /** Framework adapter: the password change and token consumption are one conditional database update. */
17
+ class PasswordReset {
18
+ constructor(model, key, fields, options, allowed, invalidate, database) {
19
+ var _a;
20
+ this.model = model;
21
+ this.key = key;
22
+ this.fields = fields;
23
+ this.options = options;
24
+ this.allowed = allowed;
25
+ this.invalidate = invalidate;
26
+ this.database = database;
27
+ this.tokenSeconds = (0, AuthSecurity_1.authLifetime)(options.tokenSeconds, 15 * 60);
28
+ this.cooldownSeconds = (0, AuthSecurity_1.authLifetime)(options.cooldownSeconds, 3 * 60);
29
+ this.revokeSessions = (_a = options.revokeSessions) !== null && _a !== void 0 ? _a : "all";
30
+ if (!["all", "scope", false].includes(this.revokeSessions))
31
+ throw new Error("Invalid password reset session policy.");
32
+ const prefix = `auth_reset_${key}_`;
33
+ this.hashField = prefix + "hash";
34
+ this.expiresField = prefix + "expires_at";
35
+ this.requestedField = prefix + "requested_at";
36
+ this.hiddenFields = [this.hashField, this.expiresField, this.requestedField];
37
+ }
38
+ rows() {
39
+ return __awaiter(this, void 0, void 0, function* () {
40
+ const instance = new this.model();
41
+ const db = yield (this.database ? this.database() : DBConnection_1.DBConnection.getDocumentStore(instance.__connection));
42
+ return db.collection(instance.__table);
43
+ });
44
+ }
45
+ digest(token, user) {
46
+ // Bind reset tokens to the scope and current password: a later password change invalidates them.
47
+ return (0, AuthSecurity_1.hashAuthToken)(`${this.key}\0${token}\0${user[this.fields.password]}`);
48
+ }
49
+ userForToken(token) {
50
+ return __awaiter(this, void 0, void 0, function* () {
51
+ if (typeof token !== "string" || !/^[A-Za-z0-9_-]{1,128}\.[A-Za-z0-9_-]{43}$/.test(token))
52
+ return null;
53
+ const subject = token.split(".")[0];
54
+ const id = this.fields.primaryKey === "_id" ? yield this.model.objectId(subject, { noExceptions: true }) : subject;
55
+ if (id == null)
56
+ return null;
57
+ const user = yield this.model.where(this.fields.primaryKey, "=", id).first();
58
+ if (!user || typeof user[this.fields.password] !== "string" || !(yield this.allowed(user)))
59
+ return null;
60
+ return user;
61
+ });
62
+ }
63
+ /** No account-existence result is returned. Deliver the raw token only through a trusted server callback. */
64
+ request(identifier, deliver) {
65
+ return __awaiter(this, void 0, void 0, function* () {
66
+ if (typeof identifier !== "string" || !identifier.trim() || identifier.length > 320)
67
+ return;
68
+ const user = yield this.model.where(this.fields.identifier, "=", identifier.trim().toLowerCase()).first();
69
+ if (!user || typeof user[this.fields.password] !== "string" || !(yield this.allowed(user)))
70
+ return;
71
+ const now = Date.now(), expiresAt = now + this.tokenSeconds * 1000;
72
+ const token = `${user[this.fields.primaryKey]}.${(0, AuthSecurity_1.randomAuthToken)()}`;
73
+ const filter = { [this.fields.primaryKey]: user[this.fields.primaryKey],
74
+ [this.fields.password]: user[this.fields.password],
75
+ $or: [{ [this.requestedField]: { $exists: false } }, { [this.requestedField]: { $lte: now - this.cooldownSeconds * 1000 } }] };
76
+ const updated = yield (yield this.rows()).findOneAndUpdate(filter, { $set: { [this.hashField]: this.digest(token, user), [this.expiresField]: expiresAt, [this.requestedField]: now } }, { returnDocument: "after" });
77
+ if (updated)
78
+ yield deliver(user, token, expiresAt);
79
+ });
80
+ }
81
+ /** A non-consuming check for the reset form; reset() always checks again atomically. */
82
+ validate(token) {
83
+ return __awaiter(this, void 0, void 0, function* () {
84
+ const user = yield this.userForToken(token);
85
+ if (!user)
86
+ return null;
87
+ const row = yield (yield this.rows()).findOne({ [this.fields.primaryKey]: user[this.fields.primaryKey],
88
+ [this.fields.password]: user[this.fields.password], [this.hashField]: this.digest(token, user), [this.expiresField]: { $gt: Date.now() } });
89
+ return row ? user : null;
90
+ });
91
+ }
92
+ reset(token, password) {
93
+ return __awaiter(this, void 0, void 0, function* () {
94
+ if (typeof password !== "string" || !password || password.length > 1024)
95
+ throw new Error("Invalid password.");
96
+ const user = yield this.validate(token);
97
+ if (!user)
98
+ return null;
99
+ const stored = user[this.fields.password];
100
+ const nextPassword = yield (0, bcryptjs_1.hash)(password, 12);
101
+ const row = yield (yield this.rows()).findOneAndUpdate(Object.assign({ [this.fields.primaryKey]: user[this.fields.primaryKey], [this.fields.password]: stored, [this.hashField]: this.digest(token, user), [this.expiresField]: { $gt: Date.now() } }, (this.fields.active === false ? {} : { [this.fields.active]: { $ne: false } })), { $set: { [this.fields.password]: nextPassword }, $unset: { [this.hashField]: "", [this.expiresField]: "" } }, { returnDocument: "after" });
102
+ if (!row)
103
+ return null;
104
+ user[this.fields.password] = nextPassword;
105
+ if (this.revokeSessions !== false)
106
+ yield this.invalidate(user, this.revokeSessions);
107
+ return user;
108
+ });
109
+ }
110
+ }
111
+ exports.PasswordReset = PasswordReset;
@@ -1,9 +1,10 @@
1
- import type { DocumentDatabase } from "../Database/DatabaseAdapter";
1
+ import type { DatabaseIdAdapter, DocumentDatabase } from "../Database/DatabaseAdapter";
2
2
  export type AuthSession = {
3
3
  id: string;
4
4
  subject: string;
5
5
  issuer: string;
6
6
  audience: string;
7
+ scope: string;
7
8
  environment: string | null;
8
9
  userAgent?: string;
9
10
  lastActiveAt?: number;
@@ -27,16 +28,21 @@ export type CreatedAuthSession = SessionTokenPair & {
27
28
  export type SessionAuthOptions = {
28
29
  issuer: string;
29
30
  audience: string;
31
+ scope?: string;
30
32
  environment?: string;
31
33
  connection?: string;
32
34
  collection?: string;
33
35
  accessTokenSeconds?: number;
34
36
  sessionSeconds?: number;
37
+ /** Maximum inactivity; false disables the idle limit. */
38
+ idleTimeoutSeconds?: number | false;
35
39
  /** Called on creation, authentication and refresh. Check account status and tenant membership here.
36
40
  * For federated sessions this can also introspect the parent authority session. Failures fail closed. */
37
41
  isSessionAllowed: (session: AuthSession) => boolean | Promise<boolean>;
38
42
  /** Dependency injection for a document-capable driver. Atomic findOneAndUpdate is required. */
39
43
  database?: () => Promise<DocumentDatabase>;
44
+ /** ID handling for an injected document store; otherwise the selected driver owns IDs. */
45
+ idAdapter?: DatabaseIdAdapter;
40
46
  };
41
47
  /** Revocable, hashed, independently addressable device sessions. Legacy UserAuth is unaffected. */
42
48
  export declare class SessionAuth {
@@ -44,8 +50,12 @@ export declare class SessionAuth {
44
50
  private readonly scope;
45
51
  private readonly accessSeconds;
46
52
  private readonly sessionSeconds;
53
+ private readonly idleSeconds;
47
54
  constructor(options: SessionAuthOptions);
55
+ private ids;
56
+ private sessionId;
48
57
  private rows;
58
+ private liveFilter;
49
59
  private allowed;
50
60
  private pair;
51
61
  create(subject: string, parent?: AuthSession["parent"]): Promise<CreatedAuthSession>;
@@ -55,6 +65,8 @@ export declare class SessionAuth {
55
65
  verifyCsrf(token: string, csrf: string, kind?: "access" | "refresh"): Promise<boolean>;
56
66
  /** One winner under concurrency. A previously consumed refresh token revokes its device session. */
57
67
  refresh(token: string): Promise<SessionTokenPair | null>;
68
+ /** Trusted browser GET resumption: renew access without consuming or replaying refresh rotation. */
69
+ renewAccess(token: string): Promise<SessionTokenPair | null>;
58
70
  /** Browser login metadata; arbitrary request data and token hashes are never exposed. */
59
71
  recordLogin(session: AuthSession, userAgent: string): Promise<void>;
60
72
  /** Persist activity with a one-minute throttle; authorization does not depend on this timestamp. */