@webtypen/webframez-core 0.3.71 → 0.3.73

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.
Files changed (105) hide show
  1. package/README.md +17 -0
  2. package/agents/webframez_rules.md +53 -0
  3. package/auth-client.d.ts +11 -0
  4. package/auth-client.js +50 -0
  5. package/dist/Auth/AuthSecurity.d.ts +11 -0
  6. package/dist/Auth/AuthSecurity.js +55 -0
  7. package/dist/Auth/ModelAuth.d.ts +72 -0
  8. package/dist/Auth/ModelAuth.js +226 -0
  9. package/dist/Auth/SessionAuth.d.ts +67 -0
  10. package/dist/Auth/SessionAuth.js +169 -0
  11. package/dist/Auth/SingleSignOn.d.ts +99 -0
  12. package/dist/Auth/SingleSignOn.js +287 -0
  13. package/dist/Auth/WebAuth.d.ts +42 -0
  14. package/dist/Auth/WebAuth.js +145 -0
  15. package/dist/Auth/index.d.ts +4 -0
  16. package/dist/Auth/index.js +4 -0
  17. package/dist/DataBuilder/DataBuilder.d.ts +32 -108
  18. package/dist/DataBuilder/DataBuilder.js +122 -411
  19. package/dist/DataBuilder/DataBuilderController.js +2 -24
  20. package/dist/DataBuilder/DataBuilderFieldPath.d.ts +1 -0
  21. package/dist/DataBuilder/DataBuilderFieldPath.js +9 -0
  22. package/dist/DataBuilder/DataBuilderFieldType.d.ts +52 -0
  23. package/dist/DataBuilder/DataBuilderFieldType.js +51 -0
  24. package/dist/DataBuilder/DataBuilderFieldsProcessor.d.ts +36 -0
  25. package/dist/DataBuilder/DataBuilderFieldsProcessor.js +235 -0
  26. package/dist/DataBuilder/DataBuilderForms.d.ts +17 -0
  27. package/dist/DataBuilder/DataBuilderForms.js +87 -0
  28. package/dist/DataBuilder/DataBuilderFrontend.d.ts +4 -0
  29. package/dist/DataBuilder/DataBuilderFrontend.js +98 -0
  30. package/dist/DataBuilder/DataBuilderRoute.d.ts +21 -0
  31. package/dist/DataBuilder/DataBuilderRoute.js +61 -0
  32. package/dist/DataBuilder/DataBuilderTypes.d.ts +86 -0
  33. package/dist/DataBuilder/DataBuilderTypes.js +2 -0
  34. package/dist/DataBuilder/DataBuilderValidationType.d.ts +10 -0
  35. package/dist/DataBuilder/DataBuilderValidationType.js +21 -0
  36. package/dist/DataBuilder/Fields/ApiAutocompleteFieldType.d.ts +5 -0
  37. package/dist/DataBuilder/Fields/ApiAutocompleteFieldType.js +12 -0
  38. package/dist/DataBuilder/Fields/ArrayFieldType.d.ts +8 -0
  39. package/dist/DataBuilder/Fields/ArrayFieldType.js +55 -0
  40. package/dist/DataBuilder/Fields/BooleanFieldType.d.ts +5 -0
  41. package/dist/DataBuilder/Fields/BooleanFieldType.js +12 -0
  42. package/dist/DataBuilder/Fields/ColorFieldType.d.ts +5 -0
  43. package/dist/DataBuilder/Fields/ColorFieldType.js +12 -0
  44. package/dist/DataBuilder/Fields/CurrencyFieldType.d.ts +5 -0
  45. package/dist/DataBuilder/Fields/CurrencyFieldType.js +12 -0
  46. package/dist/DataBuilder/Fields/CustomFieldType.d.ts +5 -0
  47. package/dist/DataBuilder/Fields/CustomFieldType.js +12 -0
  48. package/dist/DataBuilder/Fields/DateFieldType.d.ts +5 -0
  49. package/dist/DataBuilder/Fields/DateFieldType.js +12 -0
  50. package/dist/DataBuilder/Fields/DaterangeFieldType.d.ts +5 -0
  51. package/dist/DataBuilder/Fields/DaterangeFieldType.js +12 -0
  52. package/dist/DataBuilder/Fields/DatetimeFieldType.d.ts +6 -0
  53. package/dist/DataBuilder/Fields/DatetimeFieldType.js +22 -0
  54. package/dist/DataBuilder/Fields/FloatFieldType.d.ts +6 -0
  55. package/dist/DataBuilder/Fields/FloatFieldType.js +15 -0
  56. package/dist/DataBuilder/Fields/IntegerFieldType.d.ts +8 -0
  57. package/dist/DataBuilder/Fields/IntegerFieldType.js +19 -0
  58. package/dist/DataBuilder/Fields/ModelFieldType.d.ts +5 -0
  59. package/dist/DataBuilder/Fields/ModelFieldType.js +12 -0
  60. package/dist/DataBuilder/Fields/NumericFieldType.d.ts +12 -0
  61. package/dist/DataBuilder/Fields/NumericFieldType.js +53 -0
  62. package/dist/DataBuilder/Fields/ObjectFieldType.d.ts +8 -0
  63. package/dist/DataBuilder/Fields/ObjectFieldType.js +45 -0
  64. package/dist/DataBuilder/Fields/ObjectIdFieldType.d.ts +8 -0
  65. package/dist/DataBuilder/Fields/ObjectIdFieldType.js +22 -0
  66. package/dist/DataBuilder/Fields/OptionFieldType.d.ts +5 -0
  67. package/dist/DataBuilder/Fields/OptionFieldType.js +12 -0
  68. package/dist/DataBuilder/Fields/PasswordFieldType.d.ts +5 -0
  69. package/dist/DataBuilder/Fields/PasswordFieldType.js +12 -0
  70. package/dist/DataBuilder/Fields/SecureFieldType.d.ts +5 -0
  71. package/dist/DataBuilder/Fields/SecureFieldType.js +12 -0
  72. package/dist/DataBuilder/Fields/StringFieldType.d.ts +5 -0
  73. package/dist/DataBuilder/Fields/StringFieldType.js +12 -0
  74. package/dist/DataBuilder/Fields/TimeFieldType.d.ts +5 -0
  75. package/dist/DataBuilder/Fields/TimeFieldType.js +12 -0
  76. package/dist/DataBuilder/Fields/WysiwygFieldType.d.ts +5 -0
  77. package/dist/DataBuilder/Fields/WysiwygFieldType.js +12 -0
  78. package/dist/DataBuilder/Fields/index.d.ts +42 -0
  79. package/dist/DataBuilder/Fields/index.js +70 -0
  80. package/dist/DataBuilder/LegacyFieldTypeAdapter.d.ts +22 -0
  81. package/dist/DataBuilder/LegacyFieldTypeAdapter.js +50 -0
  82. package/dist/DataBuilder/ModelForm.d.ts +23 -0
  83. package/dist/DataBuilder/ModelForm.js +21 -0
  84. package/dist/DataBuilder/Validations/EmailValidationType.d.ts +5 -0
  85. package/dist/DataBuilder/Validations/EmailValidationType.js +16 -0
  86. package/dist/DataBuilder/Validations/LengthValidationType.d.ts +5 -0
  87. package/dist/DataBuilder/Validations/LengthValidationType.js +20 -0
  88. package/dist/DataBuilder/Validations/MaxValidationType.d.ts +5 -0
  89. package/dist/DataBuilder/Validations/MaxValidationType.js +18 -0
  90. package/dist/DataBuilder/Validations/MinValidationType.d.ts +5 -0
  91. package/dist/DataBuilder/Validations/MinValidationType.js +18 -0
  92. package/dist/DataBuilder/Validations/index.d.ts +6 -0
  93. package/dist/DataBuilder/Validations/index.js +15 -0
  94. package/dist/DataBuilder/decorators.d.ts +10 -0
  95. package/dist/DataBuilder/decorators.js +64 -0
  96. package/dist/DataBuilder/formScope.d.ts +7 -0
  97. package/dist/DataBuilder/formScope.js +107 -0
  98. package/dist/Router/Request.d.ts +2 -0
  99. package/dist/Router/Route.d.ts +6 -0
  100. package/dist/Router/Route.js +17 -0
  101. package/dist/index.d.ts +8 -0
  102. package/dist/index.js +13 -0
  103. package/docs/authentication.md +366 -0
  104. package/docs/databuilder.md +183 -0
  105. package/package.json +7 -2
package/README.md CHANGED
@@ -9,6 +9,7 @@ This README reflects the current API in this repository and focuses on:
9
9
  - DataBuilder
10
10
  - Console Commands
11
11
  - Queue and Jobs
12
+ - Session, browser and cross-instance authentication
12
13
 
13
14
  ## Requirements
14
15
 
@@ -21,6 +22,16 @@ This README reflects the current API in this repository and focuses on:
21
22
  npm i @webtypen/webframez-core
22
23
  ```
23
24
 
25
+ ## Authentication
26
+
27
+ New integrations can use `SessionAuth` for hashed, revocable device sessions,
28
+ `WebAuth` for host-only browser cookies and CSRF protection, and
29
+ `SsoAuthority` / `SsoClient` for first-party cross-instance login with single-use
30
+ codes, PKCE and central session introspection. These APIs are opt-in; existing
31
+ `UserAuth` consumers keep their current behavior.
32
+
33
+ See [Authentication: setup, security model and migration](docs/authentication.md).
34
+
24
35
  ## Database drivers (from 0.3.69)
25
36
 
26
37
  The core has no MongoDB/BSON dependency. HTTP routing and database-independent
@@ -68,6 +79,7 @@ Die Regeln beschreiben unter anderem:
68
79
  - Nutzung von Model.objectId(...) statt eigener ObjectId-Resolver
69
80
  - bevorzugte Nutzung der vorhandenen Helper wie StringFunctions, NumericFunctions und DateFunctions
70
81
  - Verwendung der Webframez-Abstraktionen fuer Request, Response, Middleware, Storage, Datatables und Routes
82
+ - sinnvolle Leerzeilen nach Imports, zwischen Eigenschaften und Methoden sowie zwischen Methoden
71
83
 
72
84
  Wichtig: AGENTS.md-Dateien haben keine universell standardisierte Import-Funktion. Die praktikable Variante ist deshalb, die Regeldatei aus dem Paket in der AGENTS.md des eigenen Projekts explizit zu referenzieren und projektspezifische Regeln lokal zu ergaenzen.
73
85
 
@@ -87,6 +99,7 @@ Insbesondere gilt:
87
99
  - fuer ObjectId-Konvertierungen Model.objectId(...) oder die passende Model-Instanzmethode verwenden
88
100
  - vorhandene Webframez-Helper und Facades bevorzugen statt neue Utilities oder Resolver anzulegen
89
101
  - fuer HTTP-, Routing-, Middleware-, Storage- und Datatable-Code die Webframez-APIs und Konventionen beibehalten
102
+ - die Leerzeilenregeln aus dem Abschnitt "Code-Formatierung und Leerzeilen" einhalten
90
103
 
91
104
  Projektspezifische Ergaenzung:
92
105
  - dieses Projekt verwendet fuer Admin-Routen zusaetzlich die Middleware "admin-auth"
@@ -908,3 +921,7 @@ a penguin glyph fallback). Set `accessibilityLabel` only when the icon adds mean
908
921
  not already present in adjacent text. Optional `size` is a positive number.
909
922
  Content is rendered as text and icons, never HTML. Plain navigation labels retain
910
923
  the text representation; their separate `icon` property controls navigation icons.
924
+
925
+ ## DataBuilder model forms
926
+
927
+ DataBuilder supports `@Field()`, parameterized `@Validates()`, server-side `ModelForm` classes and `Route.databuilder()` while retaining the v1 request protocol and legacy controller registrations. See [DataBuilder documentation](docs/databuilder.md).
@@ -15,6 +15,59 @@ Codex soll in Webframez-Projekten bevorzugt bestehende Framework-Abstraktionen n
15
15
  5. Neue Abstraktionen nur dann erzeugen, wenn die Framework-API die Anforderung nachweislich nicht abbildet.
16
16
  6. Controller-, Route-, Middleware-, Datatable- und Storage-Loesungen sollen idiomatisch fuer Webframez sein und nicht wie portierter Express-Code aussehen.
17
17
 
18
+ ## Code-Formatierung und Leerzeilen
19
+
20
+ Diese Regeln gelten fuer neuen und bearbeiteten TypeScript-/JavaScript-Code im Core und in Webframez-Projekten. Verwende sinnvolle Leerzeilen, damit zusammengehoerige Bloecke erkennbar bleiben:
21
+
22
+ 1. Nach dem letzten Import folgt genau eine Leerzeile vor der ersten Deklaration bzw. ihrem zugehoerigen Kommentar oder Decorator.
23
+ 2. In Klassen folgt genau eine Leerzeile zwischen einem Block von Eigenschaften und dem ersten Konstruktor, Getter, Setter oder der ersten Methode.
24
+ 3. Zwischen einzelnen Konstruktoren, Methoden, Gettern und Settern steht genau eine Leerzeile. Zusammengehoerige TypeScript-Overload-Signaturen bleiben mit ihrer Implementierung als Gruppe zusammen.
25
+ 4. Zwischen eigenstaendigen Top-Level-Klassen, Funktionen, Interfaces und Typdefinitionen steht genau eine Leerzeile. Zugehoerige Kommentare und Decorators bleiben direkt an der Deklaration.
26
+ 5. Zusammengehoerige Eigenschaften, etwa `key` und `type`, bleiben ohne zusaetzliche Leerzeilen zusammen. Innerhalb von Methoden trennen Leerzeilen fachlich unterschiedliche Schritte; nicht jede einzelne Anweisung wird abgesetzt.
27
+ 6. Leerzeilen enthalten keine Leerzeichen oder Tabs. Keine mehrfachen Leerzeilen zur kuenstlichen Streckung verwenden. Vorhandene Einrueckungs- und Formatter-Vorgaben beibehalten.
28
+ 7. Die Abstaende beim Schreiben bzw. Bearbeiten direkt einhalten und vor Abschluss pruefen. Ein Formatter ersetzt diese Regel nicht, wenn er die erforderlichen Leerzeilen nicht selbst einfuegt. Unbeteiligte Dateien nicht allein fuer eine flaechenweite Formatierung umschreiben.
29
+
30
+ Beispiel fuer den Abstand nach Imports:
31
+
32
+ ```ts
33
+ import { FloatFieldType } from "./FloatFieldType";
34
+
35
+ export class CurrencyFieldType extends FloatFieldType {
36
+ key = "currency";
37
+ type = "currency";
38
+ }
39
+ ```
40
+
41
+ Beispiel fuer Eigenschaften und Methoden:
42
+
43
+ ```ts
44
+ import { DataBuilderFieldType, DataBuilderFieldContext, DataBuilderValidationContext } from "../DataBuilderFieldType";
45
+
46
+ export class ObjectFieldType extends DataBuilderFieldType {
47
+ key = "object";
48
+ type = "object";
49
+
50
+ missingValue(context: DataBuilderFieldContext): any {
51
+ return context.structured && context.field.schema ? this.convert({}, context) : null;
52
+ }
53
+
54
+ async validate(value: any, context: DataBuilderValidationContext) {
55
+ const error = await super.validate(value, context);
56
+ if (error) return error;
57
+ return context.structured && !this.isMissing(value) && (typeof value !== "object" || Array.isArray(value))
58
+ ? "Bitte ein Objekt angeben."
59
+ : null;
60
+ }
61
+
62
+ async convert(value: any, context: DataBuilderFieldContext) {
63
+ if (context.structured && context.field.schema) {
64
+ return context.applyChildren(context.field.schema, { ...(context.currentValue || {}) }, context.path);
65
+ }
66
+ return value;
67
+ }
68
+ }
69
+ ```
70
+
18
71
  ## Imports
19
72
 
20
73
  Bevorzuge immer dieses Muster:
@@ -0,0 +1,11 @@
1
+ /** Public browser-only entry point for ModelAuth / Route.auth. */
2
+ export type AuthClientOptions = {
3
+ basePath?: string;
4
+ loginPaths?: string[];
5
+ cookieName?: string;
6
+ requiresCsrf?: (url: URL) => boolean;
7
+ };
8
+
9
+ export declare function createAuthFetch(options?: AuthClientOptions, originalFetch?: typeof fetch): typeof fetch;
10
+
11
+ export declare function installAuthForms(options?: AuthClientOptions & { fetch?: typeof fetch }): () => void;
package/auth-client.js ADDED
@@ -0,0 +1,50 @@
1
+ /** Browser-only entry point. No server or database modules are loaded. */
2
+ function csrfCookie(name) {
3
+ const cookieName = name || (window.location.protocol === "https:" ? "__Host-wf_csrf" : "wf_dev_csrf");
4
+ const values = document.cookie.split(";").map(part => part.trim()).filter(part => part.startsWith(`${cookieName}=`));
5
+ return values.length === 1 ? decodeURIComponent(values[0].slice(cookieName.length + 1)) : "";
6
+ }
7
+
8
+ exports.createAuthFetch = function createAuthFetch(options = {}, originalFetch = window.fetch.bind(window)) {
9
+ const base = (options.basePath || "/api/auth").replace(/\/+$/, "");
10
+ const loginPaths = options.loginPaths || [`${base}/login`];
11
+ return async (input, init) => {
12
+ const url = new URL(input instanceof Request ? input.url : String(input), window.location.href);
13
+ const method = String(init?.method || (input instanceof Request ? input.method : "GET")).toUpperCase();
14
+ if (url.origin !== window.location.origin || ["GET", "HEAD", "OPTIONS"].includes(method)
15
+ || (options.requiresCsrf && !options.requiresCsrf(url))) return originalFetch(input, init);
16
+ let token;
17
+ if (loginPaths.includes(url.pathname)) {
18
+ const bootstrap = await originalFetch(`${base}/csrf`, { credentials: "same-origin", cache: "no-store" });
19
+ if (!bootstrap.ok) return bootstrap;
20
+ token = (await bootstrap.json()).csrf;
21
+ } else token = csrfCookie(options.cookieName);
22
+ const headers = new Headers(init?.headers || (input instanceof Request ? input.headers : undefined));
23
+ if (typeof token === "string" && token) headers.set("X-CSRF-Token", token);
24
+ return originalFetch(input, { ...init, headers });
25
+ };
26
+ };
27
+
28
+ exports.installAuthForms = function installAuthForms(options = {}) {
29
+ const loginPaths = options.loginPaths || ["/api/auth/login"];
30
+ const fetch = options.fetch || window.fetch.bind(window);
31
+ const listener = event => {
32
+ const form = event.target;
33
+ if (!(form instanceof HTMLFormElement) || form.method.toLowerCase() !== "post") return;
34
+ const url = new URL(form.action);
35
+ if (url.origin !== window.location.origin || !loginPaths.includes(url.pathname)) return;
36
+ const token = csrfCookie(options.cookieName);
37
+ if (!token) {
38
+ event.preventDefault();
39
+ const submitter = event.submitter;
40
+ void fetch(`${(options.basePath || "/api/auth").replace(/\/+$/, "")}/csrf`, { credentials: "same-origin", cache: "no-store" })
41
+ .then(response => { if (response.ok && csrfCookie(options.cookieName)) form.requestSubmit(submitter); });
42
+ return;
43
+ }
44
+ let field = form.querySelector('input[name="_csrf"]');
45
+ if (!field) { field = document.createElement("input"); field.type = "hidden"; field.name = "_csrf"; form.append(field); }
46
+ field.value = token;
47
+ };
48
+ document.addEventListener("submit", listener, true);
49
+ return () => document.removeEventListener("submit", listener, true);
50
+ };
@@ -0,0 +1,11 @@
1
+ /// <reference types="node" />
2
+ import type { Request } from "../Router/Request";
3
+ /** Internal primitives shared by session, browser and SSO authentication. */
4
+ export declare function randomAuthToken(): string;
5
+ export declare function hashAuthToken(value: string): string;
6
+ export declare function equalAuthToken(left: unknown, right: unknown): boolean;
7
+ export declare function authText(value: unknown, name: string, max?: number): string;
8
+ export declare function authLifetime(value: number | undefined, fallback: number): number;
9
+ export declare function authHeader(req: Pick<Request, "headers">, name: string): string;
10
+ export declare function authCookie(req: Pick<Request, "headers">, name: string): string;
11
+ export declare function authUrl(value: string, allowInsecureLocalhost?: boolean): URL;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authUrl = exports.authCookie = exports.authHeader = exports.authLifetime = exports.authText = exports.equalAuthToken = exports.hashAuthToken = exports.randomAuthToken = void 0;
4
+ const crypto_1 = require("crypto");
5
+ /** Internal primitives shared by session, browser and SSO authentication. */
6
+ function randomAuthToken() { return (0, crypto_1.randomBytes)(32).toString("base64url"); }
7
+ exports.randomAuthToken = randomAuthToken;
8
+ function hashAuthToken(value) { return (0, crypto_1.createHash)("sha256").update(value).digest("hex"); }
9
+ exports.hashAuthToken = hashAuthToken;
10
+ function equalAuthToken(left, right) {
11
+ if (typeof left !== "string" || typeof right !== "string" || left.length > 4096 || right.length > 4096)
12
+ return false;
13
+ const a = Buffer.from(left), b = Buffer.from(right);
14
+ return a.length === b.length && (0, crypto_1.timingSafeEqual)(a, b);
15
+ }
16
+ exports.equalAuthToken = equalAuthToken;
17
+ function authText(value, name, max = 512) {
18
+ if (typeof value !== "string" || !value.length || value.length > max || /[\x00-\x20\x7f]/.test(value))
19
+ throw new Error(`Invalid ${name}.`);
20
+ return value;
21
+ }
22
+ exports.authText = authText;
23
+ function authLifetime(value, fallback) {
24
+ const result = value === undefined ? fallback : value;
25
+ if (!Number.isSafeInteger(result) || result <= 0)
26
+ throw new Error("Auth lifetimes must be positive integer seconds.");
27
+ return result;
28
+ }
29
+ exports.authLifetime = authLifetime;
30
+ function authHeader(req, name) {
31
+ const key = Object.keys(req.headers || {}).find(key => key.toLowerCase() === name.toLowerCase());
32
+ const value = key ? req.headers[key] : undefined;
33
+ return typeof value === "string" ? value : "";
34
+ }
35
+ exports.authHeader = authHeader;
36
+ function authCookie(req, name) {
37
+ const matches = authHeader(req, "cookie").split(";").map(part => part.trim()).filter(part => part.startsWith(`${name}=`));
38
+ if (matches.length !== 1)
39
+ return ""; // Reject ambiguous/shadowed cookies.
40
+ try {
41
+ return decodeURIComponent(matches[0].slice(name.length + 1));
42
+ }
43
+ catch (_a) {
44
+ return "";
45
+ }
46
+ }
47
+ exports.authCookie = authCookie;
48
+ function authUrl(value, allowInsecureLocalhost = false) {
49
+ const url = new URL(value);
50
+ const local = allowInsecureLocalhost && url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
51
+ if ((!local && url.protocol !== "https:") || url.username || url.password || url.hash)
52
+ throw new Error("Auth URLs require HTTPS and must not contain credentials or fragments.");
53
+ return url;
54
+ }
55
+ exports.authUrl = authUrl;
@@ -0,0 +1,72 @@
1
+ import { Model } from "../Database/Model";
2
+ import { Request } from "../Router/Request";
3
+ import type { Response } from "../Router/Response";
4
+ import { AuthSession, SessionAuthOptions } from "./SessionAuth";
5
+ import { WebAuth, WebAuthError, WebAuthOptions } from "./WebAuth";
6
+ export type AuthContext<TUser extends Model = Model> = {
7
+ user: TUser;
8
+ session: AuthSession;
9
+ };
10
+ export type AuthSnapshot<TUser = Record<string, unknown>> = {
11
+ user: TUser;
12
+ session: AuthSession;
13
+ };
14
+ export type AuthErrorCode = "missing_login" | "invalid_login" | "inactive_user" | "invalid_origin" | "invalid_csrf" | "unauthorized" | "server_error";
15
+ export declare class AuthLoginError extends WebAuthError {
16
+ readonly code: AuthErrorCode;
17
+ constructor(code: AuthErrorCode, status: 401 | 403, message: string);
18
+ }
19
+ export type ModelAuthOptions = Partial<WebAuthOptions> & {
20
+ model: typeof Model;
21
+ /** Takes precedence over session.audience; defaults to the configured origin. */
22
+ audience?: string;
23
+ locale?: "en" | "de";
24
+ /** Record browser metadata and update activity at most once per minute. */
25
+ trackActivity?: boolean;
26
+ fields?: {
27
+ identifier?: string;
28
+ password?: string;
29
+ active?: string | false;
30
+ primaryKey?: string;
31
+ };
32
+ session?: Partial<Omit<SessionAuthOptions, "issuer" | "isSessionAllowed">>;
33
+ csrfDisabled?: false;
34
+ verifyPassword?: (password: string, storedHash: string) => boolean | Promise<boolean>;
35
+ isUserAllowed?: (user: Model) => boolean | Promise<boolean>;
36
+ onLogin?: (session: AuthSession, req: Request) => void | Promise<void>;
37
+ publicUserFields?: readonly string[];
38
+ onAuthenticate?: (auth: AuthContext, req: Request) => void | Promise<void>;
39
+ messages?: Partial<Record<AuthErrorCode, string>>;
40
+ };
41
+ export type AuthOperation = "csrf" | "login" | "logout" | "refresh";
42
+ export type AuthRouteOptions = Partial<ModelAuthOptions> & {
43
+ auth?: ModelAuth | (() => ModelAuth);
44
+ loginPath?: string;
45
+ loginResponse?: (req: Request, session: AuthSession) => object | Promise<object>;
46
+ middleware?: string[];
47
+ domains?: string[];
48
+ };
49
+ /** Model-backed browser authentication with secure default routes and revocable sessions. */
50
+ export declare class ModelAuth extends WebAuth {
51
+ private readonly modelOptions;
52
+ private readonly model;
53
+ private readonly sessionUsers;
54
+ private readonly fields;
55
+ constructor(modelOptions: ModelAuthOptions);
56
+ private message;
57
+ /** Use this when creating users; existing bcrypt hashes are supported without migration. */
58
+ static hashPassword(password: string): Promise<string>;
59
+ verifyCredentials(req: Request): Promise<string>;
60
+ login(req: Request, res: Response, verifyCredentials?: () => Promise<string | null>): Promise<AuthSession>;
61
+ /** 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>;
63
+ resolveCookies<TUser extends Model = Model>(cookies: Record<string, string | undefined>): Promise<AuthContext<TUser> | null>;
64
+ /** Only explicit public fields are sent to browsers; Model instances remain on the server. */
65
+ snapshot(auth: AuthContext | null | undefined): AuthSnapshot | null;
66
+ /** Optional auth for public routes, or required auth for protected controllers. */
67
+ middleware(options?: {
68
+ required?: boolean;
69
+ }): (next: Function, reject: Function, req: Request, res: Response) => Promise<any>;
70
+ handle(operation: AuthOperation, req: Request, res: Response, loginResponse?: AuthRouteOptions["loginResponse"]): Promise<Response>;
71
+ }
72
+ export declare function authRoute(operation: AuthOperation, options: AuthRouteOptions): (req: Request, res: Response) => Promise<Response>;
@@ -0,0 +1,226 @@
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.ModelAuth = exports.AuthLoginError = void 0;
13
+ const bcryptjs_1 = require("bcryptjs");
14
+ const Config_1 = require("../Config");
15
+ const Request_1 = require("../Router/Request");
16
+ const AuthSecurity_1 = require("./AuthSecurity");
17
+ const SessionAuth_1 = require("./SessionAuth");
18
+ const WebAuth_1 = require("./WebAuth");
19
+ class AuthLoginError extends WebAuth_1.WebAuthError {
20
+ constructor(code, status, message) {
21
+ super(status, message);
22
+ this.code = code;
23
+ }
24
+ }
25
+ exports.AuthLoginError = AuthLoginError;
26
+ const defaultMessages = {
27
+ en: {
28
+ missing_login: "Please provide your login and password.",
29
+ invalid_login: "Invalid login or password.",
30
+ inactive_user: "This account is inactive.",
31
+ invalid_origin: "The login address does not match the configured website URL.",
32
+ invalid_csrf: "Please reload the page and try logging in again.",
33
+ unauthorized: "Please log in.",
34
+ server_error: "Authentication could not be processed.",
35
+ },
36
+ de: {
37
+ missing_login: "Bitte gib E-Mail-Adresse und Passwort an.",
38
+ invalid_login: "Es wurde kein Benutzer mit dieser E-Mail-/Passwort-Kombination gefunden.",
39
+ inactive_user: "Bitte aktiviere deinen Account über die E-Mail, die wir dir geschickt haben.",
40
+ invalid_origin: "Die Login-Adresse stimmt nicht mit der konfigurierten Website-URL überein.",
41
+ invalid_csrf: "Der Login konnte gerade nicht verarbeitet werden. Bitte lade die Seite neu.",
42
+ unauthorized: "Bitte melde dich an.",
43
+ server_error: "Die Anmeldung konnte gerade nicht verarbeitet werden.",
44
+ },
45
+ };
46
+ // Verify a dummy password hash for unknown accounts as well.
47
+ let dummyHash;
48
+ /** Model-backed browser authentication with secure default routes and revocable sessions. */
49
+ class ModelAuth extends WebAuth_1.WebAuth {
50
+ constructor(modelOptions) {
51
+ var _a, _b, _c, _d, _e;
52
+ if (modelOptions.csrfDisabled)
53
+ throw new Error("Browser auth routes require CSRF protection.");
54
+ if (!modelOptions.model)
55
+ throw new Error("Route.auth requires a Model class.");
56
+ 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;
57
+ const model = modelOptions.model;
58
+ const fields = Object.assign({ identifier: "email", password: "password", active: "is_active", primaryKey: new model().__primaryKey || "_id" }, modelOptions.fields);
59
+ const allowed = (user) => __awaiter(this, void 0, void 0, function* () {
60
+ return Boolean(user && (fields.active === false || user[fields.active] !== false)
61
+ && (!modelOptions.isUserAllowed || (yield modelOptions.isUserAllowed(user))));
62
+ });
63
+ 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* () {
65
+ const id = fields.primaryKey === "_id" ? yield model.objectId(session.subject, { noExceptions: true }) : session.subject;
66
+ const user = id !== null && id !== undefined ? yield model.where(fields.primaryKey, "=", id).first() : null;
67
+ if (!(yield allowed(user)))
68
+ return false;
69
+ sessionUsers.set(session, user);
70
+ return true;
71
+ }) }));
72
+ super(sessions, Object.assign(Object.assign({}, modelOptions), { origin, allowInsecureLocalhost: (_e = modelOptions.allowInsecureLocalhost) !== null && _e !== void 0 ? _e : true }));
73
+ this.modelOptions = modelOptions;
74
+ this.sessionUsers = sessionUsers;
75
+ this.model = model;
76
+ this.fields = fields;
77
+ }
78
+ message(code) {
79
+ var _a, _b;
80
+ return (_b = (_a = this.modelOptions.messages) === null || _a === void 0 ? void 0 : _a[code]) !== null && _b !== void 0 ? _b : defaultMessages[this.modelOptions.locale || "en"][code];
81
+ }
82
+ /** Use this when creating users; existing bcrypt hashes are supported without migration. */
83
+ static hashPassword(password) {
84
+ return (0, bcryptjs_1.hash)(password, 12);
85
+ }
86
+ verifyCredentials(req) {
87
+ var _a, _b;
88
+ return __awaiter(this, void 0, void 0, function* () {
89
+ const identifier = (_a = req.body) === null || _a === void 0 ? void 0 : _a[this.fields.identifier], password = (_b = req.body) === null || _b === void 0 ? void 0 : _b.password;
90
+ if (typeof identifier !== "string" || !identifier.trim() || identifier.length > 320
91
+ || typeof password !== "string" || !password || password.length > 1024) {
92
+ throw new AuthLoginError("missing_login", 401, this.message("missing_login"));
93
+ }
94
+ const user = yield this.model.where(this.fields.identifier, "=", identifier.trim().toLowerCase()).first();
95
+ const stored = user === null || user === void 0 ? void 0 : user[this.fields.password];
96
+ const verify = this.modelOptions.verifyPassword || bcryptjs_1.compare;
97
+ const valid = yield verify(password, typeof stored === "string" ? stored : yield (dummyHash || (dummyHash = (0, bcryptjs_1.hash)("webframez-invalid-account", 12))));
98
+ if (!user || typeof stored !== "string" || !valid)
99
+ throw new AuthLoginError("invalid_login", 401, this.message("invalid_login"));
100
+ if ((this.fields.active !== false && user[this.fields.active] === false)
101
+ || (this.modelOptions.isUserAllowed && !(yield this.modelOptions.isUserAllowed(user)))) {
102
+ throw new AuthLoginError("inactive_user", 403, this.message("inactive_user"));
103
+ }
104
+ const id = user[this.fields.primaryKey];
105
+ if (id === null || id === undefined || id === "")
106
+ throw new Error("Auth user has no primary key.");
107
+ return String(id);
108
+ });
109
+ }
110
+ login(req, res, verifyCredentials = () => this.verifyCredentials(req)) {
111
+ const _super = Object.create(null, {
112
+ login: { get: () => super.login }
113
+ });
114
+ return __awaiter(this, void 0, void 0, function* () {
115
+ const session = yield _super.login.call(this, req, res, verifyCredentials);
116
+ if (this.modelOptions.trackActivity !== false)
117
+ yield this.sessions.recordLogin(session, (0, AuthSecurity_1.authHeader)(req, "user-agent"));
118
+ if (this.modelOptions.onLogin)
119
+ yield this.modelOptions.onLogin(session, req);
120
+ return session;
121
+ });
122
+ }
123
+ /** Resolve a fresh validated session and its model; never trusts a client-supplied req.auth. */
124
+ resolve(req) {
125
+ const _super = Object.create(null, {
126
+ authenticate: { get: () => super.authenticate }
127
+ });
128
+ return __awaiter(this, void 0, void 0, function* () {
129
+ Object.defineProperty(req, "auth", { value: null, writable: true, configurable: true, enumerable: false });
130
+ const session = yield _super.authenticate.call(this, req);
131
+ if (!session)
132
+ return null;
133
+ const user = this.sessionUsers.get(session);
134
+ if (!user)
135
+ return null;
136
+ if (this.modelOptions.trackActivity !== false)
137
+ yield this.sessions.touch(session);
138
+ const auth = { user, session };
139
+ if (this.modelOptions.onAuthenticate)
140
+ yield this.modelOptions.onAuthenticate(auth, req);
141
+ req.auth = auth;
142
+ return auth;
143
+ });
144
+ }
145
+ resolveCookies(cookies) {
146
+ return __awaiter(this, void 0, void 0, function* () {
147
+ const req = Object.assign(new Request_1.Request(), { method: "GET", headers: { cookie: `${this.cookies.prefix}access=${encodeURIComponent(cookies[`${this.cookies.prefix}access`] || "")}` } });
148
+ return this.resolve(req);
149
+ });
150
+ }
151
+ /** Only explicit public fields are sent to browsers; Model instances remain on the server. */
152
+ snapshot(auth) {
153
+ var _a;
154
+ if (!auth)
155
+ return null;
156
+ const fields = this.modelOptions.publicUserFields || [this.fields.primaryKey, "email", "name", "firstname", "lastname", "roles"];
157
+ const user = {};
158
+ for (const field of fields) {
159
+ 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))
161
+ continue;
162
+ const value = auth.user[field];
163
+ if (value !== undefined)
164
+ user[field] = value;
165
+ }
166
+ return JSON.parse(JSON.stringify({ user, session: auth.session }));
167
+ }
168
+ /** Optional auth for public routes, or required auth for protected controllers. */
169
+ middleware(options = {}) {
170
+ return (next, reject, req, res) => __awaiter(this, void 0, void 0, function* () {
171
+ try {
172
+ const auth = yield this.resolve(req);
173
+ res.header("Cache-Control", "no-store");
174
+ if (!auth && options.required) {
175
+ res.status(401).send({ status: "error", code: "unauthorized" });
176
+ res.end();
177
+ return reject("Unauthorized");
178
+ }
179
+ return next(true);
180
+ }
181
+ catch (error) {
182
+ if (!(error instanceof WebAuth_1.WebAuthError))
183
+ throw error;
184
+ res.status(error.status).send({ status: "error", code: "invalid_csrf" });
185
+ res.end();
186
+ return reject(error.message);
187
+ }
188
+ });
189
+ }
190
+ handle(operation, req, res, loginResponse) {
191
+ return __awaiter(this, void 0, void 0, function* () {
192
+ res.header("Cache-Control", "no-store");
193
+ try {
194
+ if (operation === "csrf")
195
+ return res.send({ csrf: this.bootstrap(req, res) });
196
+ if (operation === "logout") {
197
+ yield this.logout(req, res);
198
+ return res.send({ status: "success" });
199
+ }
200
+ if (operation === "refresh") {
201
+ yield this.refresh(req, res);
202
+ return res.send({ status: "success" });
203
+ }
204
+ const session = yield this.login(req, res);
205
+ return res.send(Object.assign(Object.assign({}, (loginResponse ? yield loginResponse(req, session) : {})), { status: "success" }));
206
+ }
207
+ catch (error) {
208
+ const code = error instanceof AuthLoginError ? error.code : error instanceof WebAuth_1.WebAuthError
209
+ ? error.message === "Invalid request origin." ? "invalid_origin" : error.status === 401 ? "unauthorized" : "invalid_csrf" : "server_error";
210
+ if (!(error instanceof WebAuth_1.WebAuthError))
211
+ console.error(error);
212
+ return res.status(error instanceof WebAuth_1.WebAuthError ? error.status : 500).send({ status: "error", code,
213
+ message: this.message(code) });
214
+ }
215
+ });
216
+ }
217
+ }
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;
@@ -0,0 +1,67 @@
1
+ import type { DocumentDatabase } from "../Database/DatabaseAdapter";
2
+ export type AuthSession = {
3
+ id: string;
4
+ subject: string;
5
+ issuer: string;
6
+ audience: string;
7
+ environment: string | null;
8
+ userAgent?: string;
9
+ lastActiveAt?: number;
10
+ createdAt: number;
11
+ expiresAt: number;
12
+ parent: {
13
+ issuer: string;
14
+ sessionId: string;
15
+ } | null;
16
+ };
17
+ export type SessionTokenPair = {
18
+ auth_token: string;
19
+ refresh_token: string;
20
+ auth_expires_at: number;
21
+ refresh_expires_at: number;
22
+ session: AuthSession;
23
+ };
24
+ export type CreatedAuthSession = SessionTokenPair & {
25
+ csrf_token: string;
26
+ };
27
+ export type SessionAuthOptions = {
28
+ issuer: string;
29
+ audience: string;
30
+ environment?: string;
31
+ connection?: string;
32
+ collection?: string;
33
+ accessTokenSeconds?: number;
34
+ sessionSeconds?: number;
35
+ /** Called on creation, authentication and refresh. Check account status and tenant membership here.
36
+ * For federated sessions this can also introspect the parent authority session. Failures fail closed. */
37
+ isSessionAllowed: (session: AuthSession) => boolean | Promise<boolean>;
38
+ /** Dependency injection for a document-capable driver. Atomic findOneAndUpdate is required. */
39
+ database?: () => Promise<DocumentDatabase>;
40
+ };
41
+ /** Revocable, hashed, independently addressable device sessions. Legacy UserAuth is unaffected. */
42
+ export declare class SessionAuth {
43
+ private readonly options;
44
+ private readonly scope;
45
+ private readonly accessSeconds;
46
+ private readonly sessionSeconds;
47
+ constructor(options: SessionAuthOptions);
48
+ private rows;
49
+ private allowed;
50
+ private pair;
51
+ create(subject: string, parent?: AuthSession["parent"]): Promise<CreatedAuthSession>;
52
+ authenticate(token: string): Promise<AuthSession | null>;
53
+ /** Trusted-server introspection; never expose this method as an unauthenticated endpoint. */
54
+ inspect(sessionId: string): Promise<AuthSession | null>;
55
+ verifyCsrf(token: string, csrf: string, kind?: "access" | "refresh"): Promise<boolean>;
56
+ /** One winner under concurrency. A previously consumed refresh token revokes its device session. */
57
+ refresh(token: string): Promise<SessionTokenPair | null>;
58
+ /** Browser login metadata; arbitrary request data and token hashes are never exposed. */
59
+ recordLogin(session: AuthSession, userAgent: string): Promise<void>;
60
+ /** Persist activity with a one-minute throttle; authorization does not depend on this timestamp. */
61
+ touch(session: AuthSession): Promise<void>;
62
+ revoke(sessionId: string): Promise<void>;
63
+ revokeAll(subject: string): Promise<void>;
64
+ list(subject: string): Promise<AuthSession[]>;
65
+ /** Run periodically; authorization checks never depend on cleanup scheduling. */
66
+ cleanup(): Promise<void>;
67
+ }