@webtypen/webframez-core 0.3.70 → 0.3.72

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 (103) hide show
  1. package/README.md +22 -0
  2. package/agents/webframez_rules.md +53 -0
  3. package/dist/Api/ApiScope.d.ts +2 -0
  4. package/dist/Api/ApiScope.js +2 -0
  5. package/dist/Api/ApiScopeRegistry.js +3 -3
  6. package/dist/Auth/AuthSecurity.d.ts +11 -0
  7. package/dist/Auth/AuthSecurity.js +55 -0
  8. package/dist/Auth/SessionAuth.d.ts +61 -0
  9. package/dist/Auth/SessionAuth.js +151 -0
  10. package/dist/Auth/SingleSignOn.d.ts +99 -0
  11. package/dist/Auth/SingleSignOn.js +287 -0
  12. package/dist/Auth/WebAuth.d.ts +42 -0
  13. package/dist/Auth/WebAuth.js +145 -0
  14. package/dist/Auth/index.d.ts +3 -0
  15. package/dist/Auth/index.js +3 -0
  16. package/dist/DataBuilder/DataBuilder.d.ts +32 -108
  17. package/dist/DataBuilder/DataBuilder.js +122 -411
  18. package/dist/DataBuilder/DataBuilderController.js +2 -24
  19. package/dist/DataBuilder/DataBuilderFieldPath.d.ts +1 -0
  20. package/dist/DataBuilder/DataBuilderFieldPath.js +9 -0
  21. package/dist/DataBuilder/DataBuilderFieldType.d.ts +52 -0
  22. package/dist/DataBuilder/DataBuilderFieldType.js +51 -0
  23. package/dist/DataBuilder/DataBuilderFieldsProcessor.d.ts +36 -0
  24. package/dist/DataBuilder/DataBuilderFieldsProcessor.js +235 -0
  25. package/dist/DataBuilder/DataBuilderForms.d.ts +17 -0
  26. package/dist/DataBuilder/DataBuilderForms.js +87 -0
  27. package/dist/DataBuilder/DataBuilderFrontend.d.ts +4 -0
  28. package/dist/DataBuilder/DataBuilderFrontend.js +98 -0
  29. package/dist/DataBuilder/DataBuilderRoute.d.ts +21 -0
  30. package/dist/DataBuilder/DataBuilderRoute.js +61 -0
  31. package/dist/DataBuilder/DataBuilderTypes.d.ts +86 -0
  32. package/dist/DataBuilder/DataBuilderTypes.js +2 -0
  33. package/dist/DataBuilder/DataBuilderValidationType.d.ts +10 -0
  34. package/dist/DataBuilder/DataBuilderValidationType.js +21 -0
  35. package/dist/DataBuilder/Fields/ApiAutocompleteFieldType.d.ts +5 -0
  36. package/dist/DataBuilder/Fields/ApiAutocompleteFieldType.js +12 -0
  37. package/dist/DataBuilder/Fields/ArrayFieldType.d.ts +8 -0
  38. package/dist/DataBuilder/Fields/ArrayFieldType.js +55 -0
  39. package/dist/DataBuilder/Fields/BooleanFieldType.d.ts +5 -0
  40. package/dist/DataBuilder/Fields/BooleanFieldType.js +12 -0
  41. package/dist/DataBuilder/Fields/ColorFieldType.d.ts +5 -0
  42. package/dist/DataBuilder/Fields/ColorFieldType.js +12 -0
  43. package/dist/DataBuilder/Fields/CurrencyFieldType.d.ts +5 -0
  44. package/dist/DataBuilder/Fields/CurrencyFieldType.js +12 -0
  45. package/dist/DataBuilder/Fields/CustomFieldType.d.ts +5 -0
  46. package/dist/DataBuilder/Fields/CustomFieldType.js +12 -0
  47. package/dist/DataBuilder/Fields/DateFieldType.d.ts +5 -0
  48. package/dist/DataBuilder/Fields/DateFieldType.js +12 -0
  49. package/dist/DataBuilder/Fields/DaterangeFieldType.d.ts +5 -0
  50. package/dist/DataBuilder/Fields/DaterangeFieldType.js +12 -0
  51. package/dist/DataBuilder/Fields/DatetimeFieldType.d.ts +6 -0
  52. package/dist/DataBuilder/Fields/DatetimeFieldType.js +22 -0
  53. package/dist/DataBuilder/Fields/FloatFieldType.d.ts +6 -0
  54. package/dist/DataBuilder/Fields/FloatFieldType.js +15 -0
  55. package/dist/DataBuilder/Fields/IntegerFieldType.d.ts +8 -0
  56. package/dist/DataBuilder/Fields/IntegerFieldType.js +19 -0
  57. package/dist/DataBuilder/Fields/ModelFieldType.d.ts +5 -0
  58. package/dist/DataBuilder/Fields/ModelFieldType.js +12 -0
  59. package/dist/DataBuilder/Fields/NumericFieldType.d.ts +12 -0
  60. package/dist/DataBuilder/Fields/NumericFieldType.js +53 -0
  61. package/dist/DataBuilder/Fields/ObjectFieldType.d.ts +8 -0
  62. package/dist/DataBuilder/Fields/ObjectFieldType.js +45 -0
  63. package/dist/DataBuilder/Fields/ObjectIdFieldType.d.ts +8 -0
  64. package/dist/DataBuilder/Fields/ObjectIdFieldType.js +22 -0
  65. package/dist/DataBuilder/Fields/OptionFieldType.d.ts +5 -0
  66. package/dist/DataBuilder/Fields/OptionFieldType.js +12 -0
  67. package/dist/DataBuilder/Fields/PasswordFieldType.d.ts +5 -0
  68. package/dist/DataBuilder/Fields/PasswordFieldType.js +12 -0
  69. package/dist/DataBuilder/Fields/SecureFieldType.d.ts +5 -0
  70. package/dist/DataBuilder/Fields/SecureFieldType.js +12 -0
  71. package/dist/DataBuilder/Fields/StringFieldType.d.ts +5 -0
  72. package/dist/DataBuilder/Fields/StringFieldType.js +12 -0
  73. package/dist/DataBuilder/Fields/TimeFieldType.d.ts +5 -0
  74. package/dist/DataBuilder/Fields/TimeFieldType.js +12 -0
  75. package/dist/DataBuilder/Fields/WysiwygFieldType.d.ts +5 -0
  76. package/dist/DataBuilder/Fields/WysiwygFieldType.js +12 -0
  77. package/dist/DataBuilder/Fields/index.d.ts +42 -0
  78. package/dist/DataBuilder/Fields/index.js +70 -0
  79. package/dist/DataBuilder/LegacyFieldTypeAdapter.d.ts +22 -0
  80. package/dist/DataBuilder/LegacyFieldTypeAdapter.js +50 -0
  81. package/dist/DataBuilder/ModelForm.d.ts +23 -0
  82. package/dist/DataBuilder/ModelForm.js +21 -0
  83. package/dist/DataBuilder/Validations/EmailValidationType.d.ts +5 -0
  84. package/dist/DataBuilder/Validations/EmailValidationType.js +16 -0
  85. package/dist/DataBuilder/Validations/LengthValidationType.d.ts +5 -0
  86. package/dist/DataBuilder/Validations/LengthValidationType.js +20 -0
  87. package/dist/DataBuilder/Validations/MaxValidationType.d.ts +5 -0
  88. package/dist/DataBuilder/Validations/MaxValidationType.js +18 -0
  89. package/dist/DataBuilder/Validations/MinValidationType.d.ts +5 -0
  90. package/dist/DataBuilder/Validations/MinValidationType.js +18 -0
  91. package/dist/DataBuilder/Validations/index.d.ts +6 -0
  92. package/dist/DataBuilder/Validations/index.js +15 -0
  93. package/dist/DataBuilder/decorators.d.ts +10 -0
  94. package/dist/DataBuilder/decorators.js +64 -0
  95. package/dist/DataBuilder/formScope.d.ts +7 -0
  96. package/dist/DataBuilder/formScope.js +107 -0
  97. package/dist/Router/Route.d.ts +3 -0
  98. package/dist/Router/Route.js +5 -0
  99. package/dist/index.d.ts +8 -0
  100. package/dist/index.js +13 -0
  101. package/docs/authentication.md +281 -0
  102. package/docs/databuilder.md +183 -0
  103. package/package.json +5 -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"
@@ -370,6 +383,11 @@ Example: `GET /api/backoffice/current-user`.
370
383
 
371
384
  The scope middleware runs before the function. Its return value is passed to the function as `apiRequest.context`. Calling `abort(message, status)` stops execution and returns an error response with that status.
372
385
 
386
+ For HTTP middleware that must also run on automatic `OPTIONS` responses, set
387
+ `routeMiddleware = ["cors"]` on the scope. These names refer to middleware in the
388
+ application kernel. They run before the route handler; scope `middleware()` still
389
+ runs for function calls and provides their authorization/context (including MCP).
390
+
373
391
  ### Group ApiScopes
374
392
 
375
393
  Use `ApiScopesGroup` to share middleware across multiple scopes:
@@ -903,3 +921,7 @@ a penguin glyph fallback). Set `accessibilityLabel` only when the icon adds mean
903
921
  not already present in adjacent text. Optional `size` is a positive number.
904
922
  Content is rendered as text and icons, never HTML. Plain navigation labels retain
905
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:
@@ -5,6 +5,8 @@ export declare class ApiScope {
5
5
  key: string;
6
6
  apiBasePath: string | null;
7
7
  mcpEndpoint: string | null;
8
+ /** Kernel middleware for HTTP routes, including automatic OPTIONS responses. */
9
+ routeMiddleware: string[];
8
10
  functions: Array<ApiFunctionClass> | (() => Array<ApiFunctionClass>);
9
11
  middleware(_req: Request, _res: Response, _abort: ApiScopeMiddlewareAbort): Promise<{
10
12
  [key: string]: any;
@@ -15,6 +15,8 @@ class ApiScope {
15
15
  this.key = "unique_scope_key";
16
16
  this.apiBasePath = "/api";
17
17
  this.mcpEndpoint = null;
18
+ /** Kernel middleware for HTTP routes, including automatic OPTIONS responses. */
19
+ this.routeMiddleware = [];
18
20
  this.functions = [];
19
21
  }
20
22
  middleware(_req, _res, _abort) {
@@ -71,10 +71,10 @@ class ApiScopeRegistryFacade {
71
71
  const handler = (req, res) => __awaiter(this, void 0, void 0, function* () {
72
72
  return yield this.handleHttpFunction(registration, FunctionClass, req, res);
73
73
  });
74
- this.registerRouteMethod(func.requestMethod, path, handler);
74
+ this.registerRouteMethod(func.requestMethod, path, handler, scope.routeMiddleware);
75
75
  }
76
76
  }
77
- registerRouteMethod(method, path, handler) {
77
+ registerRouteMethod(method, path, handler, middleware) {
78
78
  if (!method) {
79
79
  return;
80
80
  }
@@ -83,7 +83,7 @@ class ApiScopeRegistryFacade {
83
83
  if (!routeMethod || typeof routeMethod !== "function") {
84
84
  throw new Error(`ApiFunction request method "${method}" is not supported by Route`);
85
85
  }
86
- routeMethod.bind(Route_1.Route)(path, handler);
86
+ routeMethod.bind(Route_1.Route)(path, handler, { middleware });
87
87
  }
88
88
  handleHttpFunction(registration, FunctionClass, req, res) {
89
89
  return __awaiter(this, void 0, void 0, function* () {
@@ -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,61 @@
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
+ createdAt: number;
9
+ expiresAt: number;
10
+ parent: {
11
+ issuer: string;
12
+ sessionId: string;
13
+ } | null;
14
+ };
15
+ export type SessionTokenPair = {
16
+ auth_token: string;
17
+ refresh_token: string;
18
+ auth_expires_at: number;
19
+ refresh_expires_at: number;
20
+ session: AuthSession;
21
+ };
22
+ export type CreatedAuthSession = SessionTokenPair & {
23
+ csrf_token: string;
24
+ };
25
+ export type SessionAuthOptions = {
26
+ issuer: string;
27
+ audience: string;
28
+ environment?: string;
29
+ connection?: string;
30
+ collection?: string;
31
+ accessTokenSeconds?: number;
32
+ sessionSeconds?: number;
33
+ /** Called on creation, authentication and refresh. Check account status and tenant membership here.
34
+ * For federated sessions this can also introspect the parent authority session. Failures fail closed. */
35
+ isSessionAllowed: (session: AuthSession) => boolean | Promise<boolean>;
36
+ /** Dependency injection for a document-capable driver. Atomic findOneAndUpdate is required. */
37
+ database?: () => Promise<DocumentDatabase>;
38
+ };
39
+ /** Revocable, hashed, independently addressable device sessions. Legacy UserAuth is unaffected. */
40
+ export declare class SessionAuth {
41
+ private readonly options;
42
+ private readonly scope;
43
+ private readonly accessSeconds;
44
+ private readonly sessionSeconds;
45
+ constructor(options: SessionAuthOptions);
46
+ private rows;
47
+ private allowed;
48
+ private pair;
49
+ create(subject: string, parent?: AuthSession["parent"]): Promise<CreatedAuthSession>;
50
+ authenticate(token: string): Promise<AuthSession | null>;
51
+ /** Trusted-server introspection; never expose this method as an unauthenticated endpoint. */
52
+ inspect(sessionId: string): Promise<AuthSession | null>;
53
+ verifyCsrf(token: string, csrf: string, kind?: "access" | "refresh"): Promise<boolean>;
54
+ /** One winner under concurrency. A previously consumed refresh token revokes its device session. */
55
+ refresh(token: string): Promise<SessionTokenPair | null>;
56
+ revoke(sessionId: string): Promise<void>;
57
+ revokeAll(subject: string): Promise<void>;
58
+ list(subject: string): Promise<AuthSession[]>;
59
+ /** Run periodically; authorization checks never depend on cleanup scheduling. */
60
+ cleanup(): Promise<void>;
61
+ }
@@ -0,0 +1,151 @@
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.SessionAuth = void 0;
13
+ const DBConnection_1 = require("../Database/DBConnection");
14
+ const AuthSecurity_1 = require("./AuthSecurity");
15
+ function publicSession(record) {
16
+ return { id: record.id, subject: record.subject, issuer: record.issuer, audience: record.audience,
17
+ environment: record.environment, createdAt: record.createdAt, expiresAt: record.expiresAt, parent: record.parent };
18
+ }
19
+ function tokenSessionId(token) {
20
+ return typeof token === "string" && /^[A-Za-z0-9_-]{43}\.[A-Za-z0-9_-]{43}$/.test(token) ? token.split(".")[0] : null;
21
+ }
22
+ /** Revocable, hashed, independently addressable device sessions. Legacy UserAuth is unaffected. */
23
+ class SessionAuth {
24
+ constructor(options) {
25
+ this.options = options;
26
+ this.scope = { issuer: (0, AuthSecurity_1.authText)(options.issuer, "issuer"), audience: (0, AuthSecurity_1.authText)(options.audience, "audience"),
27
+ environment: options.environment === undefined ? null : (0, AuthSecurity_1.authText)(options.environment, "environment") };
28
+ this.accessSeconds = (0, AuthSecurity_1.authLifetime)(options.accessTokenSeconds, 15 * 60);
29
+ this.sessionSeconds = (0, AuthSecurity_1.authLifetime)(options.sessionSeconds, 30 * 86400);
30
+ if (typeof options.isSessionAllowed !== "function")
31
+ throw new Error("SessionAuth requires isSessionAllowed.");
32
+ }
33
+ rows() {
34
+ return __awaiter(this, void 0, void 0, function* () {
35
+ const db = yield (this.options.database ? this.options.database() : DBConnection_1.DBConnection.getDocumentStore(this.options.connection));
36
+ return db.collection(this.options.collection || "auth_sessions");
37
+ });
38
+ }
39
+ allowed(record) {
40
+ return __awaiter(this, void 0, void 0, function* () {
41
+ if (!record || record.revokedAt !== null || !Number.isFinite(record.expiresAt) || record.expiresAt <= Date.now())
42
+ return null;
43
+ const session = publicSession(record);
44
+ return (yield this.options.isSessionAllowed(session)) ? session : null;
45
+ });
46
+ }
47
+ pair(record, access, refresh) {
48
+ return { auth_token: access, refresh_token: refresh, auth_expires_at: record.accessExpiresAt,
49
+ refresh_expires_at: record.expiresAt, session: publicSession(record) };
50
+ }
51
+ create(subject, parent = null) {
52
+ return __awaiter(this, void 0, void 0, function* () {
53
+ (0, AuthSecurity_1.authText)(subject, "subject");
54
+ if (parent) {
55
+ (0, AuthSecurity_1.authText)(parent.issuer, "parent issuer");
56
+ (0, AuthSecurity_1.authText)(parent.sessionId, "parent session");
57
+ }
58
+ const now = Date.now(), id = (0, AuthSecurity_1.randomAuthToken)();
59
+ const access = `${id}.${(0, AuthSecurity_1.randomAuthToken)()}`, refresh = `${id}.${(0, AuthSecurity_1.randomAuthToken)()}`, csrf = (0, AuthSecurity_1.randomAuthToken)();
60
+ const record = Object.assign(Object.assign({ _id: id, id, subject }, this.scope), { parent: parent ? Object.assign({}, parent) : null, createdAt: now, expiresAt: now + this.sessionSeconds * 1000, accessExpiresAt: now + Math.min(this.accessSeconds, this.sessionSeconds) * 1000, accessHash: (0, AuthSecurity_1.hashAuthToken)(access), refreshHash: (0, AuthSecurity_1.hashAuthToken)(refresh), csrfHash: (0, AuthSecurity_1.hashAuthToken)(csrf), usedRefreshHashes: [], revokedAt: null });
61
+ if (!(yield this.allowed(record)))
62
+ throw new Error("Session is not allowed.");
63
+ yield (yield this.rows()).insertOne(record);
64
+ return Object.assign(Object.assign({}, this.pair(record, access, refresh)), { csrf_token: csrf });
65
+ });
66
+ }
67
+ authenticate(token) {
68
+ return __awaiter(this, void 0, void 0, function* () {
69
+ const id = tokenSessionId(token);
70
+ if (!id)
71
+ return null;
72
+ const row = yield (yield this.rows()).findOne(Object.assign(Object.assign({ _id: id }, this.scope), { accessHash: (0, AuthSecurity_1.hashAuthToken)(token), accessExpiresAt: { $gt: Date.now() } }));
73
+ return this.allowed(row);
74
+ });
75
+ }
76
+ /** Trusted-server introspection; never expose this method as an unauthenticated endpoint. */
77
+ inspect(sessionId) {
78
+ return __awaiter(this, void 0, void 0, function* () {
79
+ if (typeof sessionId !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(sessionId))
80
+ return null;
81
+ return this.allowed(yield (yield this.rows()).findOne(Object.assign({ _id: sessionId }, this.scope)));
82
+ });
83
+ }
84
+ verifyCsrf(token, csrf, kind = "access") {
85
+ return __awaiter(this, void 0, void 0, function* () {
86
+ const id = tokenSessionId(token);
87
+ if (!id || typeof csrf !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(csrf))
88
+ return false;
89
+ const row = yield (yield this.rows()).findOne(Object.assign(Object.assign(Object.assign(Object.assign({ _id: id }, this.scope), (kind === "access" ? { accessHash: (0, AuthSecurity_1.hashAuthToken)(token) } : { $or: [{ refreshHash: (0, AuthSecurity_1.hashAuthToken)(token) }, { usedRefreshHashes: (0, AuthSecurity_1.hashAuthToken)(token) }] })), { csrfHash: (0, AuthSecurity_1.hashAuthToken)(csrf) }), (kind === "access" ? { accessExpiresAt: { $gt: Date.now() } } : {})));
90
+ return !!(yield this.allowed(row));
91
+ });
92
+ }
93
+ /** One winner under concurrency. A previously consumed refresh token revokes its device session. */
94
+ refresh(token) {
95
+ return __awaiter(this, void 0, void 0, function* () {
96
+ const id = tokenSessionId(token);
97
+ if (!id)
98
+ return null;
99
+ const rows = yield this.rows(), hash = (0, AuthSecurity_1.hashAuthToken)(token);
100
+ const row = yield rows.findOne(Object.assign({ _id: id }, this.scope));
101
+ if (!(yield this.allowed(row)) || !row)
102
+ return null;
103
+ if (row.refreshHash !== hash) {
104
+ if (row.usedRefreshHashes.includes(hash))
105
+ yield this.revoke(id);
106
+ return null;
107
+ }
108
+ // Bound history size and retain an absolute expiry instead of extending sessions indefinitely.
109
+ if (row.usedRefreshHashes.length >= 4096) {
110
+ yield this.revoke(id);
111
+ return null;
112
+ }
113
+ const access = `${id}.${(0, AuthSecurity_1.randomAuthToken)()}`, refresh = `${id}.${(0, AuthSecurity_1.randomAuthToken)()}`;
114
+ const next = yield rows.findOneAndUpdate(Object.assign(Object.assign({ _id: id }, this.scope), { refreshHash: hash, revokedAt: null, expiresAt: { $gt: Date.now() } }), { $set: { accessHash: (0, AuthSecurity_1.hashAuthToken)(access), refreshHash: (0, AuthSecurity_1.hashAuthToken)(refresh),
115
+ accessExpiresAt: Math.min(Date.now() + this.accessSeconds * 1000, row.expiresAt),
116
+ usedRefreshHashes: [...row.usedRefreshHashes, hash] } }, { returnDocument: "after" });
117
+ if (!next) {
118
+ // Also detect simultaneous replay that lost the compare-and-swap.
119
+ yield rows.updateOne(Object.assign(Object.assign({ _id: id }, this.scope), { usedRefreshHashes: hash, revokedAt: null }), { $set: { revokedAt: Date.now() } });
120
+ return null;
121
+ }
122
+ return this.pair(next, access, refresh);
123
+ });
124
+ }
125
+ revoke(sessionId) {
126
+ return __awaiter(this, void 0, void 0, function* () {
127
+ (0, AuthSecurity_1.authText)(sessionId, "session ID");
128
+ yield (yield this.rows()).updateOne(Object.assign(Object.assign({ _id: sessionId }, this.scope), { revokedAt: null }), { $set: { revokedAt: Date.now() } });
129
+ });
130
+ }
131
+ revokeAll(subject) {
132
+ return __awaiter(this, void 0, void 0, function* () {
133
+ (0, AuthSecurity_1.authText)(subject, "subject");
134
+ yield (yield this.rows()).updateMany(Object.assign(Object.assign({ subject }, this.scope), { revokedAt: null }), { $set: { revokedAt: Date.now() } });
135
+ });
136
+ }
137
+ list(subject) {
138
+ return __awaiter(this, void 0, void 0, function* () {
139
+ (0, AuthSecurity_1.authText)(subject, "subject");
140
+ const rows = yield (yield this.rows()).find(Object.assign(Object.assign({ subject }, this.scope), { revokedAt: null, expiresAt: { $gt: Date.now() } })).toArray();
141
+ return rows.map(publicSession);
142
+ });
143
+ }
144
+ /** Run periodically; authorization checks never depend on cleanup scheduling. */
145
+ cleanup() {
146
+ return __awaiter(this, void 0, void 0, function* () {
147
+ yield (yield this.rows()).deleteMany(Object.assign(Object.assign({}, this.scope), { expiresAt: { $lte: Date.now() } }));
148
+ });
149
+ }
150
+ }
151
+ exports.SessionAuth = SessionAuth;
@@ -0,0 +1,99 @@
1
+ import type { DocumentDatabase } from "../Database/DatabaseAdapter";
2
+ import type { Request } from "../Router/Request";
3
+ import type { Response } from "../Router/Response";
4
+ import { AuthSession, SessionAuth } from "./SessionAuth";
5
+ import { AuthCookies, WebAuthOptions } from "./WebAuth";
6
+ /** Generate once during trusted instance provisioning; store only secretHash at the authority. */
7
+ export declare function createSsoClientCredentials(): {
8
+ id: string;
9
+ secret: string;
10
+ secretHash: string;
11
+ };
12
+ export declare function readSsoClientCredentials(req: Request): {
13
+ clientId: string;
14
+ clientSecret: string;
15
+ } | null;
16
+ export type SsoClientRegistration = {
17
+ id: string;
18
+ secretHash: string;
19
+ redirectUris: string[];
20
+ };
21
+ export type SsoAuthorizationRequest = {
22
+ clientId: string;
23
+ redirectUri: string;
24
+ environment: string;
25
+ state: string;
26
+ codeChallenge: string;
27
+ codeChallengeMethod: "S256";
28
+ };
29
+ export type SsoCodeExchange = {
30
+ clientId: string;
31
+ clientSecret: string;
32
+ redirectUri: string;
33
+ environment: string;
34
+ code: string;
35
+ codeVerifier: string;
36
+ };
37
+ export type SsoIdentity = {
38
+ issuer: string;
39
+ audience: string;
40
+ subject: string;
41
+ environment: string;
42
+ authoritySessionId: string;
43
+ expiresAt: number;
44
+ };
45
+ export type SsoAuthorityOptions = {
46
+ issuer: string;
47
+ sessions: SessionAuth;
48
+ getClient: (clientId: string) => Promise<SsoClientRegistration | null>;
49
+ /** Required on issuance, exchange and introspection: verify the instance owns the environment AND the user is a member. */
50
+ authorize: (session: AuthSession, client: SsoClientRegistration, environment: string) => Promise<boolean>;
51
+ connection?: string;
52
+ collection?: string;
53
+ database?: () => Promise<DocumentDatabase>;
54
+ allowInsecureLocalhost?: boolean;
55
+ };
56
+ /** First-party, confidential-client SSO code bridge. Not a full OAuth/OIDC provider. */
57
+ export declare class SsoAuthority {
58
+ private readonly options;
59
+ readonly issuer: string;
60
+ constructor(options: SsoAuthorityOptions);
61
+ private rows;
62
+ private client;
63
+ private authenticatedClient;
64
+ /** The caller authenticates the central browser session; its access token never leaves the authority. */
65
+ authorize(accessToken: string, request: SsoAuthorizationRequest): Promise<string | null>;
66
+ /** Server-to-server only; registered client credentials and PKCE are both mandatory. */
67
+ exchange(request: SsoCodeExchange): Promise<SsoIdentity | null>;
68
+ /** Use from instance session policy for central logout/account/membership revocation. */
69
+ introspect(clientId: string, clientSecret: string, sessionId: string, environment: string): Promise<SsoIdentity | null>;
70
+ cleanup(): Promise<void>;
71
+ }
72
+ export type SsoClientOptions = WebAuthOptions & {
73
+ issuer: string;
74
+ clientId: string;
75
+ clientSecret: string;
76
+ authorizationEndpoint: string;
77
+ tokenEndpoint: string;
78
+ introspectionEndpoint: string;
79
+ redirectUri: string;
80
+ connection?: string;
81
+ collection?: string;
82
+ database?: () => Promise<DocumentDatabase>;
83
+ };
84
+ /** Instance-side redirect flow. state + server-held PKCE verifier + host-only browser-binding cookie. */
85
+ export declare class SsoClient {
86
+ private readonly options;
87
+ readonly cookies: AuthCookies;
88
+ readonly issuer: string;
89
+ private readonly authorization;
90
+ private readonly token;
91
+ private readonly introspection;
92
+ constructor(options: SsoClientOptions);
93
+ private rows;
94
+ begin(res: Response, environment: string): Promise<string>;
95
+ private identity;
96
+ complete(req: Request, res: Response): Promise<SsoIdentity | null>;
97
+ introspect(sessionId: string, environment: string): Promise<SsoIdentity | null>;
98
+ cleanup(): Promise<void>;
99
+ }