@naturalcycles/backend-lib 9.74.0 → 9.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,9 @@
1
1
  import type { BackendRequestHandler } from '../server/server.model.js';
2
2
  import type { BaseAdminService } from './base.admin.service.js';
3
3
  import type { FirebaseSharedServiceCfg } from './firebase.shared.service.js';
4
+ /**
5
+ * @deprecated Use `AuthLoginRedirectMiddlewareCfg`.
6
+ */
4
7
  export interface RequireAdminCfg {
5
8
  /**
6
9
  * @default '/login.html'
@@ -25,14 +28,28 @@ export interface RequireAdminCfg {
25
28
  */
26
29
  andComparison?: boolean;
27
30
  }
31
+ /**
32
+ * @deprecated Use `AuthMiddleware`.
33
+ */
28
34
  export type AdminMiddleware = (reqPermissions?: string[], cfg?: RequireAdminCfg) => BackendRequestHandler;
35
+ /**
36
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
37
+ */
29
38
  export declare function createAdminMiddleware(adminService: BaseAdminService, cfgDefaults?: RequireAdminCfg): AdminMiddleware;
30
39
  /**
31
40
  * Require Admin permission.
32
41
  * If not authenticated - will redirect to `loginHtmlPath` (default to /login.html).
33
42
  * If authenticated, but not authorized - will throw 403.
34
43
  * Otherwise will just pass.
44
+ *
45
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
35
46
  */
36
47
  export declare function requireAdminPermissions(adminService: BaseAdminService, reqPermissions?: string[], cfg?: RequireAdminCfg): BackendRequestHandler;
48
+ /**
49
+ * @deprecated Use `loginHtmlHandler`.
50
+ */
37
51
  export declare function loginHtml(firebaseServiceCfg: FirebaseSharedServiceCfg): BackendRequestHandler;
52
+ /**
53
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
54
+ */
38
55
  export declare function getLoginHtmlRedirect(href: string): string;
@@ -2,6 +2,9 @@ import { _memoized } from '@naturalcycles/js-lib/decorators';
2
2
  import { AppError } from '@naturalcycles/js-lib/error/error.util.js';
3
3
  import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
4
4
  import { srcDir } from '../paths.cnst.js';
5
+ /**
6
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
7
+ */
5
8
  export function createAdminMiddleware(adminService, cfgDefaults = {}) {
6
9
  return (reqPermissions, cfg) => requireAdminPermissions(adminService, reqPermissions, {
7
10
  ...cfgDefaults,
@@ -13,6 +16,8 @@ export function createAdminMiddleware(adminService, cfgDefaults = {}) {
13
16
  * If not authenticated - will redirect to `loginHtmlPath` (default to /login.html).
14
17
  * If authenticated, but not authorized - will throw 403.
15
18
  * Otherwise will just pass.
19
+ *
20
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
16
21
  */
17
22
  export function requireAdminPermissions(adminService, reqPermissions = [], cfg = {}) {
18
23
  const { loginHtmlPath = '/login.html', urlStartsWith, apiHost, autoLogin = true, andComparison = true, } = cfg;
@@ -36,6 +41,9 @@ export function requireAdminPermissions(adminService, reqPermissions = [], cfg =
36
41
  }
37
42
  };
38
43
  }
44
+ /**
45
+ * @deprecated Use `loginHtmlHandler`.
46
+ */
39
47
  export function loginHtml(firebaseServiceCfg) {
40
48
  const { apiKey: firebaseApiKey, authDomain: firebaseAuthDomain, adminAuthProvider: firebaseAuthProvider = 'GoogleAuthProvider', } = firebaseServiceCfg;
41
49
  return (_req, res) => {
@@ -54,6 +62,9 @@ const getLoginHtml = _memoized((cfg) => {
54
62
  .replaceAll('<%= firebaseAuthDomain %>', cfg.firebaseAuthDomain)
55
63
  .replaceAll('<%= firebaseAuthProvider %>', cfg.firebaseAuthProvider);
56
64
  });
65
+ /**
66
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
67
+ */
57
68
  export function getLoginHtmlRedirect(href) {
58
69
  return `
59
70
  <html>
@@ -3,6 +3,8 @@ import type { Auth } from 'firebase-admin/auth';
3
3
  import type { BackendRequest, BackendRequestHandler } from '../server/server.model.js';
4
4
  /**
5
5
  * Base implementation based on Firebase Auth tokens passed as 'admin_token' cookie.
6
+ *
7
+ * @deprecated Use `AuthService`, with `FirebaseAuthService` for `authenticate`.
6
8
  */
7
9
  export declare class BaseAdminService {
8
10
  private loadFirebaseAuth;
@@ -61,6 +63,9 @@ export declare class BaseAdminService {
61
63
  */
62
64
  getFirebaseAuthLoginHandler(): BackendRequestHandler;
63
65
  }
66
+ /**
67
+ * @deprecated Use `AuthServiceCfg` and `FirebaseAuthServiceCfg`.
68
+ */
64
69
  export interface AdminServiceCfg {
65
70
  /**
66
71
  * @default 'admin_token'
@@ -73,16 +78,25 @@ export interface AdminServiceCfg {
73
78
  */
74
79
  authEnabled?: boolean;
75
80
  }
81
+ /**
82
+ * @deprecated Use `PermissionInfo`.
83
+ */
76
84
  export interface AdminInfo {
77
85
  email: string;
78
86
  permissions: string[];
79
87
  }
88
+ /**
89
+ * @deprecated Use `ResolvedAuth`.
90
+ */
80
91
  export interface ResolvedAdmin {
81
92
  /** undefined - no valid admin token */
82
93
  email?: string;
83
94
  /** undefined - not an Admin */
84
95
  permissions?: Set<string>;
85
96
  }
97
+ /**
98
+ * @deprecated Use `AuthCheckOptions`.
99
+ */
86
100
  export interface CheckPermissionsOptions {
87
101
  /**
88
102
  * false - one granted permission is enough.
@@ -91,6 +105,9 @@ export interface CheckPermissionsOptions {
91
105
  */
92
106
  andComparison?: boolean;
93
107
  }
108
+ /**
109
+ * @deprecated Use `AuthService.has` / `AuthService.require`.
110
+ */
94
111
  export interface CheckPermissionsResult {
95
112
  /** undefined - no valid admin token */
96
113
  email?: string;
@@ -4,6 +4,8 @@ import { _assert } from '@naturalcycles/js-lib/error';
4
4
  import { dimGrey, green, red } from '@naturalcycles/nodejs-lib/colors';
5
5
  /**
6
6
  * Base implementation based on Firebase Auth tokens passed as 'admin_token' cookie.
7
+ *
8
+ * @deprecated Use `AuthService`, with `FirebaseAuthService` for `authenticate`.
7
9
  */
8
10
  export class BaseAdminService {
9
11
  loadFirebaseAuth;
@@ -0,0 +1,41 @@
1
+ import type { Auth } from 'firebase-admin/auth';
2
+ import type { BackendRequest, BackendRequestHandler } from '../server/server.model.js';
3
+ import type { FirebaseSharedServiceCfg } from './firebase.shared.service.js';
4
+ /**
5
+ * Verifies Firebase Auth tokens.
6
+ */
7
+ export declare class FirebaseAuthService {
8
+ private loadFirebaseAuth;
9
+ cfg: FirebaseAuthServiceCfg;
10
+ constructor(loadFirebaseAuth: () => Promise<FirebaseTokenVerifier>, cfg: FirebaseAuthServiceCfg);
11
+ /**
12
+ * Reads the configured cookie first, then the header.
13
+ */
14
+ getToken(req: BackendRequest): string | undefined;
15
+ /**
16
+ * Returns undefined if there's no token, or it's expired.
17
+ * Rejects on any other verification failure.
18
+ */
19
+ getEmailByToken(token: string | undefined): Promise<string | undefined>;
20
+ private getFirebaseAuth;
21
+ /**
22
+ * POST handler that stores the token from the `Authentication` header in `tokenCookie` (not validated).
23
+ * `Authentication: logout` clears it. The bundled login.html posts to `/admin/login`.
24
+ */
25
+ getLoginHandler(): BackendRequestHandler;
26
+ }
27
+ /**
28
+ * Serves login.html, configured for Firebase Auth.
29
+ */
30
+ export declare function loginHtmlHandler(firebaseServiceCfg: FirebaseSharedServiceCfg): BackendRequestHandler;
31
+ /**
32
+ * The part of Firebase `Auth` that FirebaseAuthService uses.
33
+ */
34
+ export type FirebaseTokenVerifier = Pick<Auth, 'verifyIdToken'>;
35
+ export interface FirebaseAuthServiceCfg {
36
+ /**
37
+ * Read by `getToken`, set by `getLoginHandler`.
38
+ */
39
+ tokenCookie?: string;
40
+ tokenHeader?: string;
41
+ }
@@ -0,0 +1,103 @@
1
+ import { __decorate } from "tslib";
2
+ import { _Memo, _memoized } from '@naturalcycles/js-lib/decorators';
3
+ import { _assert } from '@naturalcycles/js-lib/error';
4
+ import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
5
+ import { srcDir } from '../paths.cnst.js';
6
+ /**
7
+ * Verifies Firebase Auth tokens.
8
+ */
9
+ export class FirebaseAuthService {
10
+ loadFirebaseAuth;
11
+ cfg;
12
+ constructor(loadFirebaseAuth, cfg) {
13
+ this.loadFirebaseAuth = loadFirebaseAuth;
14
+ this.cfg = cfg;
15
+ }
16
+ /**
17
+ * Reads the configured cookie first, then the header.
18
+ */
19
+ getToken(req) {
20
+ const { tokenCookie, tokenHeader } = this.cfg;
21
+ if (tokenCookie) {
22
+ const token = req.cookies?.[tokenCookie];
23
+ if (token)
24
+ return token;
25
+ }
26
+ if (tokenHeader)
27
+ return req.header(tokenHeader);
28
+ }
29
+ /**
30
+ * Returns undefined if there's no token, or it's expired.
31
+ * Rejects on any other verification failure.
32
+ */
33
+ async getEmailByToken(token) {
34
+ if (!token)
35
+ return;
36
+ const auth = await this.getFirebaseAuth();
37
+ try {
38
+ const decodedToken = await auth.verifyIdToken(token);
39
+ return decodedToken.email;
40
+ }
41
+ catch (err) {
42
+ if (err?.hasCode?.('id-token-expired'))
43
+ return;
44
+ throw err;
45
+ }
46
+ }
47
+ async getFirebaseAuth() {
48
+ return await this.loadFirebaseAuth();
49
+ }
50
+ /**
51
+ * POST handler that stores the token from the `Authentication` header in `tokenCookie` (not validated).
52
+ * `Authentication: logout` clears it. The bundled login.html posts to `/admin/login`.
53
+ */
54
+ getLoginHandler() {
55
+ const { tokenCookie } = this.cfg;
56
+ _assert(tokenCookie, 'getLoginHandler requires tokenCookie to be configured');
57
+ return async (req, res) => {
58
+ const token = req.header('authentication');
59
+ _assert(token, `401 Unauthenticated`, {
60
+ userFriendly: true,
61
+ backendResponseStatusCode: 401,
62
+ });
63
+ let maxAge = 1000 * 60 * 60 * 24 * 30; // 30 days
64
+ if (token === 'logout') {
65
+ // delete the cookie
66
+ maxAge = 0;
67
+ }
68
+ res
69
+ .cookie(tokenCookie, token, {
70
+ maxAge,
71
+ sameSite: 'lax', // can be: none, lax, strict
72
+ // comment these 2 lines to debug on localhost
73
+ httpOnly: true,
74
+ secure: true,
75
+ })
76
+ .status(204)
77
+ .end();
78
+ };
79
+ }
80
+ }
81
+ __decorate([
82
+ _Memo()
83
+ ], FirebaseAuthService.prototype, "getFirebaseAuth", null);
84
+ /**
85
+ * Serves login.html, configured for Firebase Auth.
86
+ */
87
+ export function loginHtmlHandler(firebaseServiceCfg) {
88
+ const { apiKey: firebaseApiKey, authDomain: firebaseAuthDomain, adminAuthProvider: firebaseAuthProvider = 'GoogleAuthProvider', } = firebaseServiceCfg;
89
+ return (_req, res) => {
90
+ res.send(getLoginHtml({
91
+ firebaseApiKey,
92
+ firebaseAuthDomain,
93
+ firebaseAuthProvider,
94
+ }));
95
+ };
96
+ }
97
+ const getLoginHtml = _memoized((cfg) => {
98
+ return fs2
99
+ .readText(`${srcDir}/admin/login.html`)
100
+ .replaceAll('<%= firebaseApiKey %>', cfg.firebaseApiKey)
101
+ .replaceAll('<%= firebaseAuthDomain %>', cfg.firebaseAuthDomain)
102
+ .replaceAll('<%= firebaseAuthProvider %>', cfg.firebaseAuthProvider);
103
+ });
@@ -1,4 +1,5 @@
1
1
  export * from './adminMiddleware.js';
2
2
  export * from './base.admin.service.js';
3
3
  export * from './firebase.shared.service.js';
4
+ export * from './firebaseAuth.service.js';
4
5
  export * from './secureHeaderMiddleware.js';
@@ -1,4 +1,5 @@
1
1
  export * from './adminMiddleware.js';
2
2
  export * from './base.admin.service.js';
3
3
  export * from './firebase.shared.service.js';
4
+ export * from './firebaseAuth.service.js';
4
5
  export * from './secureHeaderMiddleware.js';
@@ -1,5 +1,8 @@
1
1
  import type { AdminMiddleware, RequireAdminCfg } from './adminMiddleware.js';
2
2
  import type { BaseAdminService } from './base.admin.service.js';
3
+ /**
4
+ * @deprecated Use `AuthLoginRedirectMiddlewareCfg` with `secureHeader`.
5
+ */
3
6
  export interface SecureHeaderMiddlewareCfg extends RequireAdminCfg {
4
7
  adminService: BaseAdminService;
5
8
  /**
@@ -14,5 +17,7 @@ export interface SecureHeaderMiddlewareCfg extends RequireAdminCfg {
14
17
  /**
15
18
  * Secures the endpoint by requiring a secret header to be present.
16
19
  * Throws Error401Admin otherwise.
20
+ *
21
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware` with `secureHeader`.
17
22
  */
18
23
  export declare function createSecureHeaderMiddleware(cfg: SecureHeaderMiddlewareCfg): AdminMiddleware;
@@ -4,6 +4,8 @@ import { requireAdminPermissions } from './adminMiddleware.js';
4
4
  /**
5
5
  * Secures the endpoint by requiring a secret header to be present.
6
6
  * Throws Error401Admin otherwise.
7
+ *
8
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware` with `secureHeader`.
7
9
  */
8
10
  export function createSecureHeaderMiddleware(cfg) {
9
11
  return reqPermissions => requireSecureHeaderOrAdmin(cfg, reqPermissions);
@@ -0,0 +1,119 @@
1
+ import type { AnyObject, Promisable } from '@naturalcycles/js-lib/types';
2
+ import type { BackendRequest, BackendRequestHandler } from '../server/server.model.js';
3
+ /**
4
+ * Authenticates requests and checks permissions.
5
+ */
6
+ export declare class AuthService<CTX = AnyObject> {
7
+ constructor(cfg: AuthServiceCfg<CTX>);
8
+ cfg: AuthServiceCfg<CTX> & {
9
+ authEnabled: boolean;
10
+ };
11
+ /**
12
+ * Responds JSON 401 / 403.
13
+ */
14
+ getMiddleware(cfg?: AuthMiddlewareCfg<CTX>): AuthMiddleware;
15
+ /**
16
+ * Like `getMiddleware`, but 401 redirects to the login page.
17
+ */
18
+ getLoginRedirectMiddleware(cfg: AuthLoginRedirectMiddlewareCfg<CTX>): AuthMiddleware;
19
+ private createHandler;
20
+ /**
21
+ * Runs `authenticate`, then `getPermissions` and `isAlwaysAuthorized` if there's a subject.
22
+ * Ignores `authEnabled`.
23
+ */
24
+ resolve(req: BackendRequest): Promise<ResolvedAuth>;
25
+ /**
26
+ * Throws 401 if there's no subject, 403 if not granted.
27
+ * Returns the granted permissions (all of the subject's if alwaysAuthorized),
28
+ * or `{ subject: 'authDisabled', permissions: [] }` if auth is disabled.
29
+ */
30
+ require(resolved: ResolvedAuth | undefined, reqPermissions?: string[], opt?: AuthCheckOptions<CTX>): PermissionInfo;
31
+ private checkSubject;
32
+ private assertAuthenticated;
33
+ private authRequiredData;
34
+ private assertGranted;
35
+ /**
36
+ * undefined - not granted.
37
+ */
38
+ private getGrantedPermissions;
39
+ private getLoginHref;
40
+ private getLoginHtmlRedirect;
41
+ }
42
+ export interface AuthServiceCfg<CTX> {
43
+ /**
44
+ * Returns the subject, or undefined if not authenticated.
45
+ */
46
+ authenticate: (req: BackendRequest) => Promise<string | undefined>;
47
+ /**
48
+ * undefined - no permission set (never granted).
49
+ */
50
+ getPermissions: (subject: string, req: BackendRequest) => Promise<string[] | undefined>;
51
+ /**
52
+ * true - every check is granted for this subject.
53
+ */
54
+ isAlwaysAuthorized?: (subject: string, permissions: string[] | undefined, req: BackendRequest) => Promisable<boolean>;
55
+ /**
56
+ * Called on every `require` check that has a subject, while auth is enabled. Must not throw.
57
+ */
58
+ onCheck?: (check: AuthCheck, ctx: (CTX & AnyObject) | undefined) => void;
59
+ /**
60
+ * false - every check is granted. For tests / local debugging. Read on every check.
61
+ *
62
+ * @default true
63
+ */
64
+ authEnabled?: boolean;
65
+ }
66
+ export interface AuthCheck {
67
+ subject: string;
68
+ granted: boolean;
69
+ alwaysAuthorized: boolean;
70
+ reqPermissions: string[];
71
+ /** The granted permissions, or reqPermissions if denied */
72
+ checkedPermissions: string[];
73
+ }
74
+ export interface AuthLoginRedirectMiddlewareCfg<CTX> extends AuthMiddlewareCfg<CTX> {
75
+ loginHtmlPath: string;
76
+ }
77
+ export interface AuthMiddlewareCfg<CTX> {
78
+ /**
79
+ * Defaults to `resolve(req)`.
80
+ */
81
+ getResolved?: (req: BackendRequest) => Promisable<ResolvedAuth | undefined>;
82
+ getCtx?: (req: BackendRequest) => (CTX & AnyObject) | undefined;
83
+ /**
84
+ * @default true
85
+ */
86
+ andComparison?: boolean;
87
+ /**
88
+ * A request with this header passes without auth. A wrong value responds 401.
89
+ */
90
+ secureHeader?: SecureHeaderCfg;
91
+ }
92
+ export interface SecureHeaderCfg {
93
+ /** e.g. 'Authorization' */
94
+ name: string;
95
+ value: string;
96
+ }
97
+ export type AuthMiddleware = (reqPermissions?: string[]) => BackendRequestHandler;
98
+ export interface AuthCheckOptions<CTX> {
99
+ /**
100
+ * false - one granted permission is enough.
101
+ *
102
+ * @default true
103
+ */
104
+ andComparison?: boolean;
105
+ /** Passed to onCheck: the app's CTX, plus any extra keys */
106
+ ctx?: CTX & AnyObject;
107
+ }
108
+ export interface PermissionInfo {
109
+ subject: string;
110
+ permissions: string[];
111
+ }
112
+ export interface ResolvedAuth {
113
+ /** undefined - not authenticated */
114
+ subject?: string;
115
+ /** undefined - no permission set */
116
+ permissions?: string[];
117
+ /** Result of `isAlwaysAuthorized` */
118
+ alwaysAuthorized?: boolean;
119
+ }
@@ -0,0 +1,147 @@
1
+ import { _assert, AppError } from '@naturalcycles/js-lib/error';
2
+ import { timingSafeStringEqual } from '@naturalcycles/nodejs-lib';
3
+ /**
4
+ * Authenticates requests and checks permissions.
5
+ */
6
+ export class AuthService {
7
+ constructor(cfg) {
8
+ this.cfg = {
9
+ authEnabled: true,
10
+ ...cfg,
11
+ };
12
+ }
13
+ cfg;
14
+ /**
15
+ * Responds JSON 401 / 403.
16
+ */
17
+ getMiddleware(cfg = {}) {
18
+ return reqPermissions => this.createHandler(cfg, reqPermissions);
19
+ }
20
+ /**
21
+ * Like `getMiddleware`, but 401 redirects to the login page.
22
+ */
23
+ getLoginRedirectMiddleware(cfg) {
24
+ const loginHref = this.getLoginHref(cfg);
25
+ return reqPermissions => this.createHandler(cfg, reqPermissions, loginHref);
26
+ }
27
+ createHandler(cfg, reqPermissions = [], loginHref) {
28
+ const { getResolved = req => this.resolve(req), getCtx, andComparison = true, secureHeader, } = cfg;
29
+ return async (req, res, next) => {
30
+ if (!this.cfg.authEnabled)
31
+ return next();
32
+ if (secureHeader) {
33
+ const providedHeader = req.get(secureHeader.name);
34
+ if (providedHeader) {
35
+ if (timingSafeStringEqual(providedHeader, secureHeader.value)) {
36
+ return next();
37
+ }
38
+ return next(new AppError('secureHeader or authentication required', this.authRequiredData()));
39
+ }
40
+ }
41
+ try {
42
+ const resolved = await getResolved(req);
43
+ if (!resolved?.subject && loginHref) {
44
+ res.status(401).send(this.getLoginHtmlRedirect(loginHref));
45
+ return;
46
+ }
47
+ this.require(resolved, reqPermissions, { andComparison, ctx: getCtx?.(req) });
48
+ return next();
49
+ }
50
+ catch (err) {
51
+ return next(err);
52
+ }
53
+ };
54
+ }
55
+ /**
56
+ * Runs `authenticate`, then `getPermissions` and `isAlwaysAuthorized` if there's a subject.
57
+ * Ignores `authEnabled`.
58
+ */
59
+ async resolve(req) {
60
+ const { authenticate, getPermissions, isAlwaysAuthorized } = this.cfg;
61
+ const subject = await authenticate(req);
62
+ if (!subject)
63
+ return {};
64
+ const permissions = await getPermissions(subject, req);
65
+ const alwaysAuthorized = isAlwaysAuthorized
66
+ ? await isAlwaysAuthorized(subject, permissions, req)
67
+ : false;
68
+ return { subject, permissions, alwaysAuthorized };
69
+ }
70
+ /**
71
+ * Throws 401 if there's no subject, 403 if not granted.
72
+ * Returns the granted permissions (all of the subject's if alwaysAuthorized),
73
+ * or `{ subject: 'authDisabled', permissions: [] }` if auth is disabled.
74
+ */
75
+ require(resolved, reqPermissions = [], opt = {}) {
76
+ if (!this.cfg.authEnabled)
77
+ return { subject: 'authDisabled', permissions: [] };
78
+ this.assertAuthenticated(resolved);
79
+ const { subject, permissions, alwaysAuthorized } = resolved;
80
+ const grantedPermissions = this.checkSubject(resolved, reqPermissions, opt);
81
+ this.assertGranted(subject, grantedPermissions, reqPermissions);
82
+ if (alwaysAuthorized) {
83
+ return { subject, permissions: permissions?.slice() || [] };
84
+ }
85
+ return { subject, permissions: grantedPermissions.slice() };
86
+ }
87
+ checkSubject(resolved, reqPermissions, opt) {
88
+ const { subject, permissions, alwaysAuthorized = false } = resolved;
89
+ const { andComparison = true, ctx } = opt;
90
+ const grantedPermissions = alwaysAuthorized
91
+ ? reqPermissions
92
+ : this.getGrantedPermissions(permissions, reqPermissions, andComparison);
93
+ this.cfg.onCheck?.({
94
+ subject,
95
+ granted: !!grantedPermissions,
96
+ alwaysAuthorized,
97
+ reqPermissions,
98
+ checkedPermissions: grantedPermissions || reqPermissions,
99
+ }, ctx);
100
+ return grantedPermissions;
101
+ }
102
+ assertAuthenticated(resolved) {
103
+ _assert(resolved?.subject, 'authentication required', this.authRequiredData());
104
+ }
105
+ authRequiredData() {
106
+ return {
107
+ authRequired: true,
108
+ backendResponseStatusCode: 401,
109
+ userFriendly: true,
110
+ };
111
+ }
112
+ assertGranted(subject, grantedPermissions, reqPermissions) {
113
+ _assert(grantedPermissions, `Permissions required: [${reqPermissions.join(', ')}]`, {
114
+ permissionsRequired: reqPermissions,
115
+ subject,
116
+ backendResponseStatusCode: 403,
117
+ userFriendly: true,
118
+ });
119
+ }
120
+ /**
121
+ * undefined - not granted.
122
+ */
123
+ getGrantedPermissions(permissions, reqPermissions, andComparison) {
124
+ if (!permissions)
125
+ return;
126
+ const grantedPermissions = reqPermissions.filter(p => permissions.includes(p));
127
+ const granted = andComparison
128
+ ? grantedPermissions.length === reqPermissions.length
129
+ : grantedPermissions.length > 0;
130
+ return granted ? grantedPermissions : undefined;
131
+ }
132
+ getLoginHref(cfg) {
133
+ return `${cfg.loginHtmlPath}?autoLogin=1&returnUrl=\${encodeURIComponent(location.href)}`;
134
+ }
135
+ getLoginHtmlRedirect(href) {
136
+ return `
137
+ <html>
138
+ <body>401 Authentication Required
139
+ <script>
140
+ document.write(\`: <a href="${href}" id="loginLink">Login</a>\`)
141
+ document.getElementById('loginLink').click()
142
+ </script>
143
+ </body>
144
+ </html>
145
+ `;
146
+ }
147
+ }
@@ -0,0 +1 @@
1
+ export * from './auth.service.js';
@@ -0,0 +1 @@
1
+ export * from './auth.service.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/backend-lib",
3
3
  "type": "module",
4
- "version": "9.74.0",
4
+ "version": "9.75.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/db-lib": "^10",
7
7
  "@naturalcycles/js-lib": "^15",
@@ -53,6 +53,8 @@
53
53
  ".": "./dist/index.js",
54
54
  "./admin": "./dist/admin/index.js",
55
55
  "./admin/*.js": "./dist/admin/*.js",
56
+ "./auth": "./dist/auth/index.js",
57
+ "./auth/*.js": "./dist/auth/*.js",
56
58
  "./db": "./dist/db/index.js",
57
59
  "./deploy": "./dist/deploy/index.js",
58
60
  "./deploy/*.js": "./dist/deploy/*.js",
@@ -6,6 +6,9 @@ import type { BackendRequestHandler } from '../server/server.model.js'
6
6
  import type { BaseAdminService } from './base.admin.service.js'
7
7
  import type { FirebaseSharedServiceCfg } from './firebase.shared.service.js'
8
8
 
9
+ /**
10
+ * @deprecated Use `AuthLoginRedirectMiddlewareCfg`.
11
+ */
9
12
  export interface RequireAdminCfg {
10
13
  /**
11
14
  * @default '/login.html'
@@ -35,11 +38,17 @@ export interface RequireAdminCfg {
35
38
  andComparison?: boolean
36
39
  }
37
40
 
41
+ /**
42
+ * @deprecated Use `AuthMiddleware`.
43
+ */
38
44
  export type AdminMiddleware = (
39
45
  reqPermissions?: string[],
40
46
  cfg?: RequireAdminCfg,
41
47
  ) => BackendRequestHandler
42
48
 
49
+ /**
50
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
51
+ */
43
52
  export function createAdminMiddleware(
44
53
  adminService: BaseAdminService,
45
54
  cfgDefaults: RequireAdminCfg = {},
@@ -56,6 +65,8 @@ export function createAdminMiddleware(
56
65
  * If not authenticated - will redirect to `loginHtmlPath` (default to /login.html).
57
66
  * If authenticated, but not authorized - will throw 403.
58
67
  * Otherwise will just pass.
68
+ *
69
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
59
70
  */
60
71
  export function requireAdminPermissions(
61
72
  adminService: BaseAdminService,
@@ -97,6 +108,9 @@ interface LoginHtmlCfg {
97
108
  firebaseAuthProvider: string
98
109
  }
99
110
 
111
+ /**
112
+ * @deprecated Use `loginHtmlHandler`.
113
+ */
100
114
  export function loginHtml(firebaseServiceCfg: FirebaseSharedServiceCfg): BackendRequestHandler {
101
115
  const {
102
116
  apiKey: firebaseApiKey,
@@ -124,6 +138,9 @@ const getLoginHtml = _memoized((cfg: LoginHtmlCfg) => {
124
138
  .replaceAll('<%= firebaseAuthProvider %>', cfg.firebaseAuthProvider)
125
139
  })
126
140
 
141
+ /**
142
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware`.
143
+ */
127
144
  export function getLoginHtmlRedirect(href: string): string {
128
145
  return `
129
146
  <html>
@@ -7,6 +7,8 @@ import type { BackendRequest, BackendRequestHandler } from '../server/server.mod
7
7
 
8
8
  /**
9
9
  * Base implementation based on Firebase Auth tokens passed as 'admin_token' cookie.
10
+ *
11
+ * @deprecated Use `AuthService`, with `FirebaseAuthService` for `authenticate`.
10
12
  */
11
13
  export class BaseAdminService {
12
14
  constructor(
@@ -309,6 +311,9 @@ const adminInfoDisabled = (): AdminInfo => ({
309
311
  permissions: [],
310
312
  })
311
313
 
314
+ /**
315
+ * @deprecated Use `AuthServiceCfg` and `FirebaseAuthServiceCfg`.
316
+ */
312
317
  export interface AdminServiceCfg {
313
318
  /**
314
319
  * @default 'admin_token'
@@ -323,11 +328,17 @@ export interface AdminServiceCfg {
323
328
  authEnabled?: boolean
324
329
  }
325
330
 
331
+ /**
332
+ * @deprecated Use `PermissionInfo`.
333
+ */
326
334
  export interface AdminInfo {
327
335
  email: string
328
336
  permissions: string[]
329
337
  }
330
338
 
339
+ /**
340
+ * @deprecated Use `ResolvedAuth`.
341
+ */
331
342
  export interface ResolvedAdmin {
332
343
  /** undefined - no valid admin token */
333
344
  email?: string
@@ -335,6 +346,9 @@ export interface ResolvedAdmin {
335
346
  permissions?: Set<string>
336
347
  }
337
348
 
349
+ /**
350
+ * @deprecated Use `AuthCheckOptions`.
351
+ */
338
352
  export interface CheckPermissionsOptions {
339
353
  /**
340
354
  * false - one granted permission is enough.
@@ -344,6 +358,9 @@ export interface CheckPermissionsOptions {
344
358
  andComparison?: boolean
345
359
  }
346
360
 
361
+ /**
362
+ * @deprecated Use `AuthService.has` / `AuthService.require`.
363
+ */
347
364
  export interface CheckPermissionsResult {
348
365
  /** undefined - no valid admin token */
349
366
  email?: string
@@ -0,0 +1,139 @@
1
+ import { _Memo, _memoized } from '@naturalcycles/js-lib/decorators'
2
+ import { _assert } from '@naturalcycles/js-lib/error'
3
+ import { fs2 } from '@naturalcycles/nodejs-lib/fs2'
4
+ import type { Auth, FirebaseAuthError } from 'firebase-admin/auth'
5
+ import { srcDir } from '../paths.cnst.js'
6
+ import type { BackendRequest, BackendRequestHandler } from '../server/server.model.js'
7
+ import type { FirebaseSharedServiceCfg } from './firebase.shared.service.js'
8
+
9
+ /**
10
+ * Verifies Firebase Auth tokens.
11
+ */
12
+ export class FirebaseAuthService {
13
+ constructor(
14
+ private loadFirebaseAuth: () => Promise<FirebaseTokenVerifier>,
15
+ public cfg: FirebaseAuthServiceCfg,
16
+ ) {}
17
+
18
+ /**
19
+ * Reads the configured cookie first, then the header.
20
+ */
21
+ getToken(req: BackendRequest): string | undefined {
22
+ const { tokenCookie, tokenHeader } = this.cfg
23
+
24
+ if (tokenCookie) {
25
+ const token = req.cookies?.[tokenCookie]
26
+ if (token) return token
27
+ }
28
+
29
+ if (tokenHeader) return req.header(tokenHeader)
30
+ }
31
+
32
+ /**
33
+ * Returns undefined if there's no token, or it's expired.
34
+ * Rejects on any other verification failure.
35
+ */
36
+ async getEmailByToken(token: string | undefined): Promise<string | undefined> {
37
+ if (!token) return
38
+
39
+ const auth = await this.getFirebaseAuth()
40
+ try {
41
+ const decodedToken = await auth.verifyIdToken(token)
42
+ return decodedToken.email
43
+ } catch (err) {
44
+ if ((err as Partial<FirebaseAuthError> | undefined)?.hasCode?.('id-token-expired')) return
45
+ throw err
46
+ }
47
+ }
48
+
49
+ @_Memo()
50
+ private async getFirebaseAuth(): Promise<FirebaseTokenVerifier> {
51
+ return await this.loadFirebaseAuth()
52
+ }
53
+
54
+ /**
55
+ * POST handler that stores the token from the `Authentication` header in `tokenCookie` (not validated).
56
+ * `Authentication: logout` clears it. The bundled login.html posts to `/admin/login`.
57
+ */
58
+ getLoginHandler(): BackendRequestHandler {
59
+ const { tokenCookie } = this.cfg
60
+ _assert(tokenCookie, 'getLoginHandler requires tokenCookie to be configured')
61
+
62
+ return async (req, res) => {
63
+ const token = req.header('authentication')
64
+ _assert(token, `401 Unauthenticated`, {
65
+ userFriendly: true,
66
+ backendResponseStatusCode: 401,
67
+ })
68
+
69
+ let maxAge = 1000 * 60 * 60 * 24 * 30 // 30 days
70
+
71
+ if (token === 'logout') {
72
+ // delete the cookie
73
+ maxAge = 0
74
+ }
75
+
76
+ res
77
+ .cookie(tokenCookie, token, {
78
+ maxAge,
79
+ sameSite: 'lax', // can be: none, lax, strict
80
+ // comment these 2 lines to debug on localhost
81
+ httpOnly: true,
82
+ secure: true,
83
+ })
84
+ .status(204)
85
+ .end()
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Serves login.html, configured for Firebase Auth.
92
+ */
93
+ export function loginHtmlHandler(
94
+ firebaseServiceCfg: FirebaseSharedServiceCfg,
95
+ ): BackendRequestHandler {
96
+ const {
97
+ apiKey: firebaseApiKey,
98
+ authDomain: firebaseAuthDomain,
99
+ adminAuthProvider: firebaseAuthProvider = 'GoogleAuthProvider',
100
+ } = firebaseServiceCfg
101
+
102
+ return (_req, res) => {
103
+ res.send(
104
+ getLoginHtml({
105
+ firebaseApiKey,
106
+ firebaseAuthDomain,
107
+ firebaseAuthProvider,
108
+ }),
109
+ )
110
+ }
111
+ }
112
+
113
+ const getLoginHtml = _memoized((cfg: LoginHtmlCfg) => {
114
+ return fs2
115
+ .readText(`${srcDir}/admin/login.html`)
116
+ .replaceAll('<%= firebaseApiKey %>', cfg.firebaseApiKey)
117
+ .replaceAll('<%= firebaseAuthDomain %>', cfg.firebaseAuthDomain)
118
+ .replaceAll('<%= firebaseAuthProvider %>', cfg.firebaseAuthProvider)
119
+ })
120
+
121
+ /**
122
+ * The part of Firebase `Auth` that FirebaseAuthService uses.
123
+ */
124
+ export type FirebaseTokenVerifier = Pick<Auth, 'verifyIdToken'>
125
+
126
+ export interface FirebaseAuthServiceCfg {
127
+ /**
128
+ * Read by `getToken`, set by `getLoginHandler`.
129
+ */
130
+ tokenCookie?: string
131
+
132
+ tokenHeader?: string
133
+ }
134
+
135
+ interface LoginHtmlCfg {
136
+ firebaseApiKey: string
137
+ firebaseAuthDomain: string
138
+ firebaseAuthProvider: string
139
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './adminMiddleware.js'
2
2
  export * from './base.admin.service.js'
3
3
  export * from './firebase.shared.service.js'
4
+ export * from './firebaseAuth.service.js'
4
5
  export * from './secureHeaderMiddleware.js'
@@ -5,6 +5,9 @@ import type { AdminMiddleware, RequireAdminCfg } from './adminMiddleware.js'
5
5
  import { requireAdminPermissions } from './adminMiddleware.js'
6
6
  import type { BaseAdminService } from './base.admin.service.js'
7
7
 
8
+ /**
9
+ * @deprecated Use `AuthLoginRedirectMiddlewareCfg` with `secureHeader`.
10
+ */
8
11
  export interface SecureHeaderMiddlewareCfg extends RequireAdminCfg {
9
12
  adminService: BaseAdminService
10
13
 
@@ -22,6 +25,8 @@ export interface SecureHeaderMiddlewareCfg extends RequireAdminCfg {
22
25
  /**
23
26
  * Secures the endpoint by requiring a secret header to be present.
24
27
  * Throws Error401Admin otherwise.
28
+ *
29
+ * @deprecated Use `AuthService.getLoginRedirectMiddleware` with `secureHeader`.
25
30
  */
26
31
  export function createSecureHeaderMiddleware(cfg: SecureHeaderMiddlewareCfg): AdminMiddleware {
27
32
  return reqPermissions => requireSecureHeaderOrAdmin(cfg, reqPermissions)
@@ -0,0 +1,303 @@
1
+ import { _assert, AppError } from '@naturalcycles/js-lib/error'
2
+ import type { ErrorData } from '@naturalcycles/js-lib/error'
3
+ import type { AnyObject, Promisable } from '@naturalcycles/js-lib/types'
4
+ import { timingSafeStringEqual } from '@naturalcycles/nodejs-lib'
5
+ import type { BackendRequest, BackendRequestHandler } from '../server/server.model.js'
6
+
7
+ /**
8
+ * Authenticates requests and checks permissions.
9
+ */
10
+ export class AuthService<CTX = AnyObject> {
11
+ constructor(cfg: AuthServiceCfg<CTX>) {
12
+ this.cfg = {
13
+ authEnabled: true,
14
+ ...cfg,
15
+ }
16
+ }
17
+
18
+ cfg: AuthServiceCfg<CTX> & { authEnabled: boolean }
19
+
20
+ /**
21
+ * Responds JSON 401 / 403.
22
+ */
23
+ getMiddleware(cfg: AuthMiddlewareCfg<CTX> = {}): AuthMiddleware {
24
+ return reqPermissions => this.createHandler(cfg, reqPermissions)
25
+ }
26
+
27
+ /**
28
+ * Like `getMiddleware`, but 401 redirects to the login page.
29
+ */
30
+ getLoginRedirectMiddleware(cfg: AuthLoginRedirectMiddlewareCfg<CTX>): AuthMiddleware {
31
+ const loginHref = this.getLoginHref(cfg)
32
+ return reqPermissions => this.createHandler(cfg, reqPermissions, loginHref)
33
+ }
34
+
35
+ private createHandler(
36
+ cfg: AuthMiddlewareCfg<CTX>,
37
+ reqPermissions: string[] = [],
38
+ loginHref?: string,
39
+ ): BackendRequestHandler {
40
+ const {
41
+ getResolved = req => this.resolve(req),
42
+ getCtx,
43
+ andComparison = true,
44
+ secureHeader,
45
+ } = cfg
46
+
47
+ return async (req, res, next) => {
48
+ if (!this.cfg.authEnabled) return next()
49
+
50
+ if (secureHeader) {
51
+ const providedHeader = req.get(secureHeader.name)
52
+ if (providedHeader) {
53
+ if (timingSafeStringEqual(providedHeader, secureHeader.value)) {
54
+ return next()
55
+ }
56
+
57
+ return next(
58
+ new AppError('secureHeader or authentication required', this.authRequiredData()),
59
+ )
60
+ }
61
+ }
62
+
63
+ try {
64
+ const resolved = await getResolved(req)
65
+ if (!resolved?.subject && loginHref) {
66
+ res.status(401).send(this.getLoginHtmlRedirect(loginHref))
67
+ return
68
+ }
69
+
70
+ this.require(resolved, reqPermissions, { andComparison, ctx: getCtx?.(req) })
71
+ return next()
72
+ } catch (err) {
73
+ return next(err)
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Runs `authenticate`, then `getPermissions` and `isAlwaysAuthorized` if there's a subject.
80
+ * Ignores `authEnabled`.
81
+ */
82
+ async resolve(req: BackendRequest): Promise<ResolvedAuth> {
83
+ const { authenticate, getPermissions, isAlwaysAuthorized } = this.cfg
84
+
85
+ const subject = await authenticate(req)
86
+ if (!subject) return {}
87
+
88
+ const permissions = await getPermissions(subject, req)
89
+ const alwaysAuthorized = isAlwaysAuthorized
90
+ ? await isAlwaysAuthorized(subject, permissions, req)
91
+ : false
92
+
93
+ return { subject, permissions, alwaysAuthorized }
94
+ }
95
+
96
+ /**
97
+ * Throws 401 if there's no subject, 403 if not granted.
98
+ * Returns the granted permissions (all of the subject's if alwaysAuthorized),
99
+ * or `{ subject: 'authDisabled', permissions: [] }` if auth is disabled.
100
+ */
101
+ require(
102
+ resolved: ResolvedAuth | undefined,
103
+ reqPermissions: string[] = [],
104
+ opt: AuthCheckOptions<CTX> = {},
105
+ ): PermissionInfo {
106
+ if (!this.cfg.authEnabled) return { subject: 'authDisabled', permissions: [] }
107
+
108
+ this.assertAuthenticated(resolved)
109
+ const { subject, permissions, alwaysAuthorized } = resolved
110
+
111
+ const grantedPermissions = this.checkSubject(resolved, reqPermissions, opt)
112
+ this.assertGranted(subject, grantedPermissions, reqPermissions)
113
+
114
+ if (alwaysAuthorized) {
115
+ return { subject, permissions: permissions?.slice() || [] }
116
+ }
117
+
118
+ return { subject, permissions: grantedPermissions.slice() }
119
+ }
120
+
121
+ private checkSubject(
122
+ resolved: AuthenticatedAuth,
123
+ reqPermissions: string[],
124
+ opt: AuthCheckOptions<CTX>,
125
+ ): string[] | undefined {
126
+ const { subject, permissions, alwaysAuthorized = false } = resolved
127
+ const { andComparison = true, ctx } = opt
128
+
129
+ const grantedPermissions = alwaysAuthorized
130
+ ? reqPermissions
131
+ : this.getGrantedPermissions(permissions, reqPermissions, andComparison)
132
+
133
+ this.cfg.onCheck?.(
134
+ {
135
+ subject,
136
+ granted: !!grantedPermissions,
137
+ alwaysAuthorized,
138
+ reqPermissions,
139
+ checkedPermissions: grantedPermissions || reqPermissions,
140
+ },
141
+ ctx,
142
+ )
143
+
144
+ return grantedPermissions
145
+ }
146
+
147
+ private assertAuthenticated(
148
+ resolved: ResolvedAuth | undefined,
149
+ ): asserts resolved is AuthenticatedAuth {
150
+ _assert(resolved?.subject, 'authentication required', this.authRequiredData())
151
+ }
152
+
153
+ private authRequiredData(): ErrorData {
154
+ return {
155
+ authRequired: true,
156
+ backendResponseStatusCode: 401,
157
+ userFriendly: true,
158
+ }
159
+ }
160
+
161
+ private assertGranted(
162
+ subject: string,
163
+ grantedPermissions: string[] | undefined,
164
+ reqPermissions: string[],
165
+ ): asserts grantedPermissions is string[] {
166
+ _assert(grantedPermissions, `Permissions required: [${reqPermissions.join(', ')}]`, {
167
+ permissionsRequired: reqPermissions,
168
+ subject,
169
+ backendResponseStatusCode: 403,
170
+ userFriendly: true,
171
+ })
172
+ }
173
+
174
+ /**
175
+ * undefined - not granted.
176
+ */
177
+ private getGrantedPermissions(
178
+ permissions: string[] | undefined,
179
+ reqPermissions: string[],
180
+ andComparison: boolean,
181
+ ): string[] | undefined {
182
+ if (!permissions) return
183
+ const grantedPermissions = reqPermissions.filter(p => permissions.includes(p))
184
+ const granted = andComparison
185
+ ? grantedPermissions.length === reqPermissions.length
186
+ : grantedPermissions.length > 0
187
+ return granted ? grantedPermissions : undefined
188
+ }
189
+
190
+ private getLoginHref(cfg: AuthLoginRedirectMiddlewareCfg<CTX>): string {
191
+ return `${cfg.loginHtmlPath}?autoLogin=1&returnUrl=\${encodeURIComponent(location.href)}`
192
+ }
193
+
194
+ private getLoginHtmlRedirect(href: string): string {
195
+ return `
196
+ <html>
197
+ <body>401 Authentication Required
198
+ <script>
199
+ document.write(\`: <a href="${href}" id="loginLink">Login</a>\`)
200
+ document.getElementById('loginLink').click()
201
+ </script>
202
+ </body>
203
+ </html>
204
+ `
205
+ }
206
+ }
207
+
208
+ export interface AuthServiceCfg<CTX> {
209
+ /**
210
+ * Returns the subject, or undefined if not authenticated.
211
+ */
212
+ authenticate: (req: BackendRequest) => Promise<string | undefined>
213
+
214
+ /**
215
+ * undefined - no permission set (never granted).
216
+ */
217
+ getPermissions: (subject: string, req: BackendRequest) => Promise<string[] | undefined>
218
+
219
+ /**
220
+ * true - every check is granted for this subject.
221
+ */
222
+ isAlwaysAuthorized?: (
223
+ subject: string,
224
+ permissions: string[] | undefined,
225
+ req: BackendRequest,
226
+ ) => Promisable<boolean>
227
+
228
+ /**
229
+ * Called on every `require` check that has a subject, while auth is enabled. Must not throw.
230
+ */
231
+ onCheck?: (check: AuthCheck, ctx: (CTX & AnyObject) | undefined) => void
232
+
233
+ /**
234
+ * false - every check is granted. For tests / local debugging. Read on every check.
235
+ *
236
+ * @default true
237
+ */
238
+ authEnabled?: boolean
239
+ }
240
+
241
+ export interface AuthCheck {
242
+ subject: string
243
+ granted: boolean
244
+ alwaysAuthorized: boolean
245
+ reqPermissions: string[]
246
+ /** The granted permissions, or reqPermissions if denied */
247
+ checkedPermissions: string[]
248
+ }
249
+
250
+ export interface AuthLoginRedirectMiddlewareCfg<CTX> extends AuthMiddlewareCfg<CTX> {
251
+ loginHtmlPath: string
252
+ }
253
+
254
+ export interface AuthMiddlewareCfg<CTX> {
255
+ /**
256
+ * Defaults to `resolve(req)`.
257
+ */
258
+ getResolved?: (req: BackendRequest) => Promisable<ResolvedAuth | undefined>
259
+ getCtx?: (req: BackendRequest) => (CTX & AnyObject) | undefined
260
+ /**
261
+ * @default true
262
+ */
263
+ andComparison?: boolean
264
+ /**
265
+ * A request with this header passes without auth. A wrong value responds 401.
266
+ */
267
+ secureHeader?: SecureHeaderCfg
268
+ }
269
+
270
+ export interface SecureHeaderCfg {
271
+ /** e.g. 'Authorization' */
272
+ name: string
273
+ value: string
274
+ }
275
+
276
+ export type AuthMiddleware = (reqPermissions?: string[]) => BackendRequestHandler
277
+
278
+ export interface AuthCheckOptions<CTX> {
279
+ /**
280
+ * false - one granted permission is enough.
281
+ *
282
+ * @default true
283
+ */
284
+ andComparison?: boolean
285
+ /** Passed to onCheck: the app's CTX, plus any extra keys */
286
+ ctx?: CTX & AnyObject
287
+ }
288
+
289
+ export interface PermissionInfo {
290
+ subject: string
291
+ permissions: string[]
292
+ }
293
+
294
+ type AuthenticatedAuth = ResolvedAuth & { subject: string }
295
+
296
+ export interface ResolvedAuth {
297
+ /** undefined - not authenticated */
298
+ subject?: string
299
+ /** undefined - no permission set */
300
+ permissions?: string[]
301
+ /** Result of `isAlwaysAuthorized` */
302
+ alwaysAuthorized?: boolean
303
+ }
@@ -0,0 +1 @@
1
+ export * from './auth.service.js'