@venizia/ignis-docs 0.0.8 → 0.2.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.
- package/README.md +7 -7
- package/content/best-practices/api-usage-examples.md +15 -12
- package/content/best-practices/architectural-patterns.md +70 -78
- package/content/best-practices/architecture-decisions.md +91 -60
- package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
- package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
- package/content/best-practices/code-style-standards/control-flow.md +5 -2
- package/content/best-practices/code-style-standards/documentation.md +13 -13
- package/content/best-practices/code-style-standards/function-patterns.md +9 -10
- package/content/best-practices/code-style-standards/index.md +1 -1
- package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
- package/content/best-practices/code-style-standards/route-definitions.md +30 -12
- package/content/best-practices/code-style-standards/tooling.md +8 -5
- package/content/best-practices/code-style-standards/type-safety.md +13 -12
- package/content/best-practices/common-pitfalls.md +56 -37
- package/content/best-practices/contribution-workflow.md +13 -14
- package/content/best-practices/data-modeling.md +46 -22
- package/content/best-practices/deployment-strategies.md +28 -27
- package/content/best-practices/error-handling.md +48 -24
- package/content/best-practices/index.md +5 -5
- package/content/best-practices/performance-optimization.md +40 -31
- package/content/best-practices/security-guidelines.md +52 -23
- package/content/best-practices/testing-strategies.md +65 -51
- package/content/best-practices/troubleshooting-tips.md +24 -24
- package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
- package/content/extensions/components/authentication/api.md +19 -19
- package/content/extensions/components/authentication/errors.md +7 -7
- package/content/extensions/components/authentication/index.md +10 -8
- package/content/extensions/components/authentication/usage.md +101 -6
- package/content/extensions/components/authorization/api.md +45 -25
- package/content/extensions/components/authorization/errors.md +6 -6
- package/content/extensions/components/authorization/index.md +11 -10
- package/content/extensions/components/authorization/usage.md +21 -21
- package/content/extensions/components/health-check.md +1 -1
- package/content/extensions/components/index.md +5 -5
- package/content/extensions/components/mail/errors.md +15 -15
- package/content/extensions/components/mail/index.md +1 -2
- package/content/extensions/components/mail/usage.md +1 -1
- package/content/extensions/components/request-tracker.md +1 -1
- package/content/extensions/components/socket-io/api.md +9 -9
- package/content/extensions/components/socket-io/errors.md +5 -5
- package/content/extensions/components/socket-io/index.md +8 -8
- package/content/extensions/components/socket-io/usage.md +1 -1
- package/content/extensions/components/static-asset/api.md +17 -4
- package/content/extensions/components/static-asset/errors.md +4 -4
- package/content/extensions/components/static-asset/index.md +26 -28
- package/content/extensions/components/static-asset/usage.md +13 -12
- package/content/extensions/components/template/index.md +2 -2
- package/content/extensions/components/template/setup-page.md +1 -1
- package/content/extensions/components/websocket/api.md +3 -3
- package/content/extensions/components/websocket/errors.md +5 -5
- package/content/extensions/components/websocket/index.md +5 -5
- package/content/extensions/components/websocket/usage.md +3 -3
- package/content/extensions/helpers/cron/index.md +2 -2
- package/content/extensions/helpers/crypto/index.md +1 -1
- package/content/extensions/helpers/env/index.md +27 -12
- package/content/extensions/helpers/error/index.md +81 -25
- package/content/extensions/helpers/index.md +2 -3
- package/content/extensions/helpers/inversion/index.md +15 -7
- package/content/extensions/helpers/kafka/compile-binary.md +92 -0
- package/content/extensions/helpers/kafka/examples.md +1 -1
- package/content/extensions/helpers/kafka/index.md +3 -0
- package/content/extensions/helpers/logger/index.md +32 -2
- package/content/extensions/helpers/network/index.md +6 -0
- package/content/extensions/helpers/queue/index.md +14 -17
- package/content/extensions/helpers/redis/index.md +548 -323
- package/content/extensions/helpers/socket-io/index.md +14 -10
- package/content/extensions/helpers/storage/api.md +44 -8
- package/content/extensions/helpers/storage/index.md +43 -7
- package/content/extensions/helpers/template/index.md +6 -3
- package/content/extensions/helpers/types/index.md +11 -8
- package/content/extensions/helpers/websocket/api.md +9 -9
- package/content/extensions/helpers/websocket/index.md +7 -7
- package/content/extensions/helpers/worker-thread/index.md +2 -2
- package/content/extensions/index.md +3 -4
- package/content/extensions/src-details/mcp-server.md +18 -24
- package/content/guides/core-concepts/application/bootstrapping.md +11 -14
- package/content/guides/core-concepts/application/index.md +3 -3
- package/content/guides/core-concepts/components.md +19 -10
- package/content/guides/core-concepts/dependency-injection.md +6 -3
- package/content/guides/core-concepts/grpc-controllers.md +6 -5
- package/content/guides/core-concepts/persistent/datasources.md +42 -43
- package/content/guides/core-concepts/persistent/index.md +16 -7
- package/content/guides/core-concepts/persistent/models.md +24 -20
- package/content/guides/core-concepts/persistent/postgres-drivers.md +201 -0
- package/content/guides/core-concepts/persistent/repositories.md +40 -23
- package/content/guides/core-concepts/persistent/search-meilisearch.md +185 -0
- package/content/guides/core-concepts/persistent/search-typesense.md +431 -0
- package/content/guides/core-concepts/persistent/transactions.md +61 -25
- package/content/guides/core-concepts/rest-controllers.md +12 -9
- package/content/guides/core-concepts/services.md +330 -60
- package/content/guides/get-started/5-minute-quickstart.md +15 -15
- package/content/guides/get-started/philosophy.md +36 -36
- package/content/guides/get-started/setup.md +3 -3
- package/content/guides/index.md +3 -3
- package/content/guides/migrations/redis-helpers-migration.md +177 -0
- package/content/guides/migrations/scoped-rbac-migration.md +17 -17
- package/content/guides/migrations/unified-connectors-migration.md +113 -0
- package/content/guides/reference/glossary.md +19 -12
- package/content/guides/reference/mcp-docs-server.md +22 -18
- package/content/guides/tutorials/building-a-crud-api.md +37 -44
- package/content/guides/tutorials/complete-installation.md +17 -17
- package/content/guides/tutorials/ecommerce-api.md +163 -124
- package/content/guides/tutorials/realtime-chat.md +181 -135
- package/content/guides/tutorials/testing.md +65 -523
- package/content/index.md +2 -180
- package/content/public/apple-touch-icon.png +0 -0
- package/content/public/og-image.png +0 -0
- package/content/public/site.webmanifest +11 -0
- package/content/references/base/application.md +4 -5
- package/content/references/base/bootstrapping.md +18 -5
- package/content/references/base/components.md +149 -120
- package/content/references/base/connectors.md +178 -0
- package/content/references/base/controllers.md +41 -30
- package/content/references/base/datasources.md +163 -92
- package/content/references/base/dependency-injection.md +34 -22
- package/content/references/base/filter-system/application-usage.md +17 -14
- package/content/references/base/filter-system/array-operators.md +7 -2
- package/content/references/base/filter-system/comparison-operators.md +3 -0
- package/content/references/base/filter-system/default-filter.md +89 -71
- package/content/references/base/filter-system/fields-order-pagination.md +22 -22
- package/content/references/base/filter-system/index.md +6 -3
- package/content/references/base/filter-system/json-filtering.md +20 -1
- package/content/references/base/filter-system/list-operators.md +1 -1
- package/content/references/base/filter-system/logical-operators.md +33 -1
- package/content/references/base/filter-system/null-operators.md +30 -1
- package/content/references/base/filter-system/quick-reference.md +23 -4
- package/content/references/base/filter-system/tips.md +5 -5
- package/content/references/base/filter-system/use-cases.md +12 -12
- package/content/references/base/grpc-controllers.md +13 -13
- package/content/references/base/index.md +24 -12
- package/content/references/base/middlewares.md +265 -327
- package/content/references/base/models.md +63 -49
- package/content/references/base/providers.md +136 -130
- package/content/references/base/repositories/advanced.md +59 -58
- package/content/references/base/repositories/index.md +115 -91
- package/content/references/base/repositories/mixins.md +55 -291
- package/content/references/base/repositories/relations.md +54 -64
- package/content/references/base/repositories/soft-deletable.md +31 -30
- package/content/references/base/services.md +296 -93
- package/content/references/configuration/environment-variables.md +49 -31
- package/content/references/configuration/index.md +6 -6
- package/content/references/index.md +17 -12
- package/content/references/quick-reference.md +65 -106
- package/content/references/utilities/crypto.md +65 -23
- package/content/references/utilities/index.md +3 -3
- package/content/references/utilities/jsx.md +6 -4
- package/content/references/utilities/module.md +68 -20
- package/content/references/utilities/parse.md +4 -14
- package/content/references/utilities/promise.md +9 -7
- package/content/references/utilities/schema.md +5 -3
- package/dist/mcp-server/common/guards.d.ts +8 -0
- package/dist/mcp-server/common/guards.d.ts.map +1 -0
- package/dist/mcp-server/common/guards.js +14 -0
- package/dist/mcp-server/common/guards.js.map +1 -0
- package/dist/mcp-server/common/index.d.ts +1 -0
- package/dist/mcp-server/common/index.d.ts.map +1 -1
- package/dist/mcp-server/common/index.js +1 -0
- package/dist/mcp-server/common/index.js.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.js +4 -2
- package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
- package/dist/mcp-server/helpers/github.helper.js +1 -1
- package/dist/mcp-server/index.js +7 -2
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/mcp-server/tools/base.tool.d.ts +6 -2
- package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/base.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
- package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
- package/package.json +9 -9
- package/content/extensions/helpers/testing/index.md +0 -510
- package/content/references/base/middleware.md +0 -347
|
@@ -432,8 +432,10 @@ extraPermissionColumns(opts?: { idType: 'string' | 'number' })
|
|
|
432
432
|
| `code` | `text` (unique) | -- | Unique permission code |
|
|
433
433
|
| `name` | `text` | -- | Permission name |
|
|
434
434
|
| `subject` | `text` | -- | Permission subject (e.g., `'User'`, `'Order'`) |
|
|
435
|
+
| `method` | `text` | -- | HTTP method (e.g., `'GET'`, `'POST'`) |
|
|
435
436
|
| `action` | `text` | -- | Permitted action (e.g., `'read'`, `'write'`) |
|
|
436
437
|
| `scope` | `text` | -- | Permission scope |
|
|
438
|
+
| `description` | `text` | `null` | Optional permission description |
|
|
437
439
|
| `parentId` | `text` or `integer` | `null` | Parent permission ID |
|
|
438
440
|
|
|
439
441
|
### extraPolicyDefinitionColumns
|
|
@@ -695,7 +697,7 @@ flowchart TD
|
|
|
695
697
|
|
|
696
698
|
```mermaid
|
|
697
699
|
flowchart LR
|
|
698
|
-
subgraph GENERATE["generate()
|
|
700
|
+
subgraph GENERATE["generate() - Token Creation"]
|
|
699
701
|
direction TB
|
|
700
702
|
P["Payload: { userId, roles, email }"]
|
|
701
703
|
P --> CHECK1{"applicationSecret?"}
|
|
@@ -705,7 +707,7 @@ flowchart LR
|
|
|
705
707
|
CHECK1 -->|"No"| PLAIN1["Use payload as-is"]
|
|
706
708
|
end
|
|
707
709
|
|
|
708
|
-
subgraph VERIFY["verify()
|
|
710
|
+
subgraph VERIFY["verify() - Token Verification"]
|
|
709
711
|
direction TB
|
|
710
712
|
T["Verified JWT payload"]
|
|
711
713
|
T --> CHECK2{"applicationSecret?"}
|
|
@@ -741,7 +743,7 @@ JWT payloads can optionally be encrypted field-by-field using AES (default `aes-
|
|
|
741
743
|
|
|
742
744
|
## Hono Context Extension
|
|
743
745
|
|
|
744
|
-
The Authentication module extends Hono's `ContextVariableMap` to provide type-safe access to auth data. Note: `ContextVariableMap` does **not** take a generic parameter
|
|
746
|
+
The Authentication module extends Hono's `ContextVariableMap` to provide type-safe access to auth data. Note: `ContextVariableMap` does **not** take a generic parameter - it is a plain interface augmentation:
|
|
745
747
|
|
|
746
748
|
```typescript
|
|
747
749
|
declare module 'hono' {
|
|
@@ -835,6 +837,8 @@ const JWTTokenPayloadSchema = z.object({
|
|
|
835
837
|
});
|
|
836
838
|
```
|
|
837
839
|
|
|
840
|
+
For the `/who-am-i` response, this schema is extended at runtime with an optional `userInformation` field (typed from `payload.getUserInformation.response.schema`, falling back to `AnyObjectSchema`) so the `withUserInformation` shape is OpenAPI-documented.
|
|
841
|
+
|
|
838
842
|
## API Endpoints
|
|
839
843
|
|
|
840
844
|
The built-in auth controller is created by the `defineAuthController()` factory function and is only available when `useAuthController: true` is set in `REST_OPTIONS`.
|
|
@@ -844,11 +848,13 @@ The built-in auth controller is created by the `defineAuthController()` factory
|
|
|
844
848
|
| `POST` | `/auth/sign-in` | No | Authenticate and receive a JWT token |
|
|
845
849
|
| `POST` | `/auth/sign-up` | Configurable | Create a new user account |
|
|
846
850
|
| `POST` | `/auth/change-password` | JWT | Change the authenticated user's password |
|
|
847
|
-
| `
|
|
851
|
+
| `POST` | `/auth/token/refresh` | JWT | Re-issue an access token using a valid JWT |
|
|
852
|
+
| `GET` | `/auth/who-am-i` | JWT | Return the current user's JWT payload (optionally with attached user information) |
|
|
853
|
+
| `GET` | `/auth/me` | JWT | Return the current user's information from `getUserInformation` |
|
|
848
854
|
| `GET` | `/certs` | No | JWKS endpoint (JWKS Issuer mode only) |
|
|
849
855
|
|
|
850
856
|
> [!NOTE]
|
|
851
|
-
> The base path `/auth` is configurable via `controllerOpts.restPath`. The `/certs` path is configurable via `rest.path` in `IJWKSIssuerOptions`. The `/certs` endpoint is intentionally unauthenticated
|
|
857
|
+
> The base path `/auth` is configurable via `controllerOpts.restPath`. The `/certs` path is configurable via `rest.path` in `IJWKSIssuerOptions`. The `/certs` endpoint is intentionally unauthenticated - it serves the public keys needed by external verifiers.
|
|
852
858
|
|
|
853
859
|
### POST /auth/sign-in
|
|
854
860
|
|
|
@@ -878,6 +884,42 @@ When `requireAuthenticatedSignUp: true`, requires JWT authentication. When `fals
|
|
|
878
884
|
|
|
879
885
|
**Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
|
|
880
886
|
|
|
887
|
+
### POST /auth/token/refresh
|
|
888
|
+
|
|
889
|
+
**Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
|
|
890
|
+
|
|
891
|
+
Re-issues an access token using the caller's currently valid JWT. There is no separate refresh token - the caller must send a valid Bearer token in the `Authorization` header.
|
|
892
|
+
|
|
893
|
+
**Request Body:** None
|
|
894
|
+
|
|
895
|
+
**Response 200:**
|
|
896
|
+
|
|
897
|
+
Uses `payload.refreshToken.response.schema` if provided, otherwise `AnyObjectSchema`. The response shape is entirely defined by your `IAuthService.refreshToken()` implementation.
|
|
898
|
+
|
|
899
|
+
**Response 501 (Not Implemented):**
|
|
900
|
+
|
|
901
|
+
Returned when the bound `IAuthService` does not implement the optional `refreshToken` method.
|
|
902
|
+
|
|
903
|
+
**Implementing `refreshToken` in your service:**
|
|
904
|
+
|
|
905
|
+
```typescript
|
|
906
|
+
export class AuthenticationService extends BaseService implements IAuthService {
|
|
907
|
+
async refreshToken(context: TContext<Env>): Promise<{ token: string }> {
|
|
908
|
+
// The current user is already verified by JWT middleware
|
|
909
|
+
const currentUser = context.get(Authentication.CURRENT_USER);
|
|
910
|
+
|
|
911
|
+
// Re-issue a new token with the same payload
|
|
912
|
+
const token = await this._tokenService.generate({ payload: currentUser });
|
|
913
|
+
return { token };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// ... signIn, signUp, changePassword ...
|
|
917
|
+
}
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
> [!NOTE]
|
|
921
|
+
> The framework does not enforce rotation or revocation policy. If you need to invalidate old tokens after refresh, implement that logic (e.g., a token blocklist or short expiry) inside your `refreshToken` implementation.
|
|
922
|
+
|
|
881
923
|
### GET /auth/who-am-i
|
|
882
924
|
|
|
883
925
|
**Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
|
|
@@ -896,6 +938,57 @@ Returns the current user's decrypted JWT payload directly from context.
|
|
|
896
938
|
}
|
|
897
939
|
```
|
|
898
940
|
|
|
941
|
+
**Query Parameters:**
|
|
942
|
+
|
|
943
|
+
| Name | Type | Description |
|
|
944
|
+
|------|------|-------------|
|
|
945
|
+
| `withUserInformation` | `true` \| `false` \| `1` \| `0` | When truthy, attaches a `userInformation` field built from `IAuthService.getUserInformation`. Defaults to `false`. |
|
|
946
|
+
|
|
947
|
+
When `withUserInformation` is truthy, the response merges the `getUserInformation` result into the payload:
|
|
948
|
+
|
|
949
|
+
```json
|
|
950
|
+
{
|
|
951
|
+
"userId": "123",
|
|
952
|
+
"roles": [{ "id": "1", "identifier": "admin", "priority": 0 }],
|
|
953
|
+
"userInformation": { "fullName": "Ada Lovelace", "department": "R&D" }
|
|
954
|
+
}
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
**Response 501 (Not Implemented):**
|
|
958
|
+
|
|
959
|
+
Returned when `withUserInformation` is truthy but the bound `IAuthService` does not implement the optional `getUserInformation` method.
|
|
960
|
+
|
|
961
|
+
### GET /auth/me
|
|
962
|
+
|
|
963
|
+
**Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
|
|
964
|
+
|
|
965
|
+
Returns the current user's information by delegating to `IAuthService.getUserInformation(context, {})`. Unlike `who-am-i`, the response is entirely the service result -- it is not merged with the JWT payload.
|
|
966
|
+
|
|
967
|
+
**Response 200:**
|
|
968
|
+
|
|
969
|
+
Uses `payload.getUserInformation.response.schema` if provided, otherwise `AnyObjectSchema`. The shape is defined by your `getUserInformation` implementation.
|
|
970
|
+
|
|
971
|
+
**Response 501 (Not Implemented):**
|
|
972
|
+
|
|
973
|
+
Returned when the bound `IAuthService` does not implement the optional `getUserInformation` method.
|
|
974
|
+
|
|
975
|
+
**Implementing `getUserInformation` in your service:**
|
|
976
|
+
|
|
977
|
+
```typescript
|
|
978
|
+
export class AuthenticationService extends BaseService implements IAuthService {
|
|
979
|
+
async getUserInformation(context: TContext<Env>, _opts: AnyObject): Promise<AnyObject> {
|
|
980
|
+
// The current user is already verified by JWT middleware
|
|
981
|
+
const currentUser = context.get(Authentication.CURRENT_USER);
|
|
982
|
+
return this.userRepository.findById({ id: currentUser.userId });
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// ... signIn, signUp, changePassword ...
|
|
986
|
+
}
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
> [!TIP]
|
|
990
|
+
> One `getUserInformation` implementation backs both routes. Use `GET /me` when you want the raw profile, or `GET /who-am-i?withUserInformation=true` when you want it merged into the principal in a single round-trip.
|
|
991
|
+
|
|
899
992
|
### GET /certs (JWKS Issuer Only)
|
|
900
993
|
|
|
901
994
|
**Authentication:** None (intentionally public)
|
|
@@ -922,7 +1015,7 @@ Returns the JSON Web Key Set for external verifiers.
|
|
|
922
1015
|
|
|
923
1016
|
## Auth Entity Column Helpers
|
|
924
1017
|
|
|
925
|
-
|
|
1018
|
+
IGNIS provides column helper functions that return pre-configured Drizzle column objects for common auth-related database tables. These functions are designed to be spread into `pgTable()` definitions, giving you standardized columns for User, Role, Permission, and PolicyDefinition entities without manually defining each column.
|
|
926
1019
|
|
|
927
1020
|
All helpers that accept an `opts` parameter support `{ idType: 'string' | 'number' }` to control whether foreign key columns use `text` (for UUIDs) or `integer` (for serial IDs). The default is `'number'`.
|
|
928
1021
|
|
|
@@ -966,8 +1059,10 @@ All helpers that accept an `opts` parameter support `{ idType: 'string' | 'numbe
|
|
|
966
1059
|
| `code` | `code` | `text` (unique) | No | -- | Unique permission code |
|
|
967
1060
|
| `name` | `name` | `text` | No | -- | Permission display name |
|
|
968
1061
|
| `subject` | `subject` | `text` | No | -- | Permission subject (e.g., `'User'`, `'Order'`) |
|
|
1062
|
+
| `method` | `method` | `text` | No | -- | HTTP method (e.g., `'GET'`, `'POST'`) |
|
|
969
1063
|
| `action` | `action` | `text` | No | -- | Permitted action (e.g., `'read'`, `'write'`) |
|
|
970
1064
|
| `scope` | `scope` | `text` | No | -- | Permission scope |
|
|
1065
|
+
| `description` | `description` | `text` | Yes | `null` | Optional permission description |
|
|
971
1066
|
| `parentId` | `parent_id` | `text` or `integer` | Yes | `null` | Parent permission ID (type depends on `idType`) |
|
|
972
1067
|
|
|
973
1068
|
### extraPolicyDefinitionColumns
|
|
@@ -119,8 +119,8 @@ classDiagram
|
|
|
119
119
|
|
|
120
120
|
class BaseFilteredAdapter~TFilter~ {
|
|
121
121
|
<<abstract>>
|
|
122
|
-
#dataSource:
|
|
123
|
-
#connector:
|
|
122
|
+
#dataSource: ICasbinPolicySource
|
|
123
|
+
#connector: TCasbinPolicyConnector
|
|
124
124
|
+loadFilteredPolicy(model, filter)* void
|
|
125
125
|
+isFiltered() boolean
|
|
126
126
|
#loadLines(opts) void
|
|
@@ -421,7 +421,7 @@ interface IAuthorizationEnforcer<
|
|
|
421
421
|
|
|
422
422
|
| Enforcer | TRules | Description |
|
|
423
423
|
|----------|--------|-------------|
|
|
424
|
-
| `CasbinAuthorizationEnforcer` | `ICasbinRules` | `{ user, lines }`
|
|
424
|
+
| `CasbinAuthorizationEnforcer` | `ICasbinRules` | `{ user, lines }` - the user plus their resolved Casbin policy lines (loaded into a pooled enforcer at evaluate time) |
|
|
425
425
|
| Custom | Any type | Your custom rules structure |
|
|
426
426
|
|
|
427
427
|
### Method Contracts
|
|
@@ -517,20 +517,20 @@ Injects `ICasbinEnforcerOptions` from the DI container using the binding key `Au
|
|
|
517
517
|
|
|
518
518
|
Called once by the registry on first use. Performs:
|
|
519
519
|
|
|
520
|
-
1. Dynamically imports `casbin`
|
|
521
|
-
2. Validates `options.model`
|
|
520
|
+
1. Dynamically imports `casbin` - throws `"casbin" is not installed` if missing.
|
|
521
|
+
2. Validates `options.model` - throws `options.model is required.` if missing.
|
|
522
522
|
3. Memoizes the payload normalizer (`options.normalizePayloadFn ?? defaultScopedPayloadFn()`).
|
|
523
523
|
4. If `cached.use`, validates `expiresIn >= MIN_EXPIRES_IN` (10,000 ms).
|
|
524
|
-
5. Builds a **`BasePoolHelper<Enforcer>`** (`size = poolSize ?? 16`, `acquireTimeoutMs = poolAcquireTimeoutMs ?? 5000`). Each pooled enforcer is created **without an adapter** (`newEnforcer(model)`
|
|
525
|
-
6. `await pool.warmup()`
|
|
524
|
+
5. Builds a **`BasePoolHelper<Enforcer>`** (`size = poolSize ?? 16`, `acquireTimeoutMs = poolAcquireTimeoutMs ?? 5000`). Each pooled enforcer is created **without an adapter** (`newEnforcer(model)` - no DB load at warmup), then `registerMatchers()` and `assertMatcherCompilesSync()` run on it.
|
|
525
|
+
6. `await pool.warmup()` - pre-creates the enforcers.
|
|
526
526
|
|
|
527
|
-
`registerMatchers()`
|
|
527
|
+
`registerMatchers()` - when `isScoped`, registers `keyMatch` as the domain matching func on `g`, adds `objectMatch` as a function, and registers it as the matching func on the resource relation (`g4`). When `domainMatching` is set (non-scoped), registers the chosen `Util.*Func` on the named role definition. Always finishes with `buildRoleLinks()`.
|
|
528
528
|
|
|
529
|
-
`assertMatcherCompilesSync()`
|
|
529
|
+
`assertMatcherCompilesSync()` - a boot-time smoke test: forces casbin's lazy matcher compile by running one dummy `enforceSync` (4 args when scoped/`normalizePayloadFn`, else 3), so a malformed matcher, an unregistered function, or an arity mismatch fails at warmup instead of on the first real request.
|
|
530
530
|
|
|
531
531
|
### destroy()
|
|
532
532
|
|
|
533
|
-
`this.pool?.destroy()`
|
|
533
|
+
`this.pool?.destroy()` - drains and disposes the pooled enforcers.
|
|
534
534
|
|
|
535
535
|
### buildRules()
|
|
536
536
|
|
|
@@ -551,7 +551,7 @@ flowchart TD
|
|
|
551
551
|
|
|
552
552
|
- **`extractUserLines(user)`** builds a fresh, **isolated** enforcer *with the adapter*, calls
|
|
553
553
|
`adapter.loadFilteredPolicy({ principal: { type, id } })`, then `extractLinesFrom()` serializes every
|
|
554
|
-
p-type and g-type rule back into lines. This throwaway enforcer never serves a request
|
|
554
|
+
p-type and g-type rule back into lines. This throwaway enforcer never serves a request - that is the
|
|
555
555
|
anti-poisoning guarantee.
|
|
556
556
|
- **`fetchLinesWithRedisCache`** returns cached lines on hit (Redis owns expiry via `PX`). On miss it
|
|
557
557
|
dedups concurrent misses through `pendingLineFetches` (single-flight), extracts once, and writes the
|
|
@@ -631,7 +631,7 @@ await opts.enforcer.buildRoleLinks();
|
|
|
631
631
|
## BaseFilteredAdapter
|
|
632
632
|
|
|
633
633
|
Thin read-only base for casbin `FilteredAdapter`s backed by a datasource. It owns the boilerplate
|
|
634
|
-
every filtered adapter repeats
|
|
634
|
+
every filtered adapter repeats - datasource/connector plumbing, the `isFiltered() === true` flag, the
|
|
635
635
|
no-op write methods, and a `loadLines` helper. A subclass implements only `loadFilteredPolicy`: query
|
|
636
636
|
the store for ONE principal's policies and turn them into casbin lines.
|
|
637
637
|
|
|
@@ -642,10 +642,10 @@ abstract class BaseFilteredAdapter<TFilter = ICasbinPolicyFilter>
|
|
|
642
642
|
extends BaseHelper
|
|
643
643
|
implements FilteredAdapter
|
|
644
644
|
{
|
|
645
|
-
protected readonly dataSource:
|
|
646
|
-
protected get connector():
|
|
645
|
+
protected readonly dataSource: ICasbinPolicySource;
|
|
646
|
+
protected get connector(): TCasbinPolicyConnector;
|
|
647
647
|
|
|
648
|
-
constructor(opts: { scope: string; dataSource:
|
|
648
|
+
constructor(opts: { scope: string; dataSource: ICasbinPolicySource });
|
|
649
649
|
|
|
650
650
|
// Subclasses implement ONLY this:
|
|
651
651
|
abstract loadFilteredPolicy(model: Model, filter: TFilter): Promise<void>;
|
|
@@ -680,9 +680,29 @@ interface ICasbinPolicyFilter {
|
|
|
680
680
|
}
|
|
681
681
|
```
|
|
682
682
|
|
|
683
|
+
### ICasbinPolicySource
|
|
684
|
+
|
|
685
|
+
The minimal contract `BaseFilteredAdapter` depends on for its `dataSource` -- **not** the framework's
|
|
686
|
+
general `IDataSource` interface. Any drizzle-backed datasource (e.g. `BasePostgresDataSource`) satisfies
|
|
687
|
+
it structurally; the adapter only ever needs the connector to run policy queries.
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
interface ICasbinPolicySource {
|
|
691
|
+
connector: TCasbinPolicyConnector;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
type TCasbinPolicyConnector = ReturnType<
|
|
695
|
+
typeof drizzle<Record<string, AnyType>, NodePgClient>
|
|
696
|
+
>;
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
> [!NOTE]
|
|
700
|
+
> Components that only need query execution depend on this minimal local contract rather than a
|
|
701
|
+
> connector class -- keeps the casbin adapters decoupled from the full datasource surface.
|
|
702
|
+
|
|
683
703
|
### loadLines()
|
|
684
704
|
|
|
685
|
-
The base's only orchestration helper
|
|
705
|
+
The base's only orchestration helper - subclasses call it from `loadFilteredPolicy` after assembling
|
|
686
706
|
their casbin lines:
|
|
687
707
|
|
|
688
708
|
```typescript
|
|
@@ -694,14 +714,14 @@ protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void
|
|
|
694
714
|
}
|
|
695
715
|
```
|
|
696
716
|
|
|
697
|
-
There are no template-method query hooks or shared line formatters on the base
|
|
717
|
+
There are no template-method query hooks or shared line formatters on the base - a subclass owns its
|
|
698
718
|
own queries and line construction (see `ScopedCasbinAdapter` below for the reference implementation).
|
|
699
719
|
|
|
700
720
|
## ScopedCasbinAdapter
|
|
701
721
|
|
|
702
722
|
The generic, read-only `FilteredAdapter` for the scoped RBAC model. It reads **one principal's edges**
|
|
703
723
|
plus the **shared structural hierarchy** from a single `PolicyDefinition` edge table (joined to
|
|
704
|
-
`Permission` for codes) and emits casbin lines. No subclassing
|
|
724
|
+
`Permission` for codes) and emits casbin lines. No subclassing - configure it with `IScopedCasbinEntities`.
|
|
705
725
|
|
|
706
726
|
### Class
|
|
707
727
|
|
|
@@ -709,7 +729,7 @@ plus the **shared structural hierarchy** from a single `PolicyDefinition` edge t
|
|
|
709
729
|
class ScopedCasbinAdapter extends BaseFilteredAdapter<IScopedCasbinPolicyFilter> {
|
|
710
730
|
protected readonly entities: IScopedCasbinEntities;
|
|
711
731
|
|
|
712
|
-
constructor(opts: { dataSource:
|
|
732
|
+
constructor(opts: { dataSource: ICasbinPolicySource; entities: IScopedCasbinEntities });
|
|
713
733
|
|
|
714
734
|
async loadFilteredPolicy(model: Model, filter: IScopedCasbinPolicyFilter): Promise<void>;
|
|
715
735
|
|
|
@@ -750,7 +770,7 @@ interface IScopedCasbinPolicyFilter {
|
|
|
750
770
|
}
|
|
751
771
|
```
|
|
752
772
|
|
|
753
|
-
### loadFilteredPolicy()
|
|
773
|
+
### loadFilteredPolicy() - two waves
|
|
754
774
|
|
|
755
775
|
```mermaid
|
|
756
776
|
flowchart TD
|
|
@@ -761,8 +781,8 @@ flowchart TD
|
|
|
761
781
|
W2 --> Load["loadLines(model, all lines)"]
|
|
762
782
|
```
|
|
763
783
|
|
|
764
|
-
1. **Wave 1 (parallel):** the principal's own edges
|
|
765
|
-
(`g2`), direct grants (`p`)
|
|
784
|
+
1. **Wave 1 (parallel):** the principal's own edges - role assignments (`g`), domain memberships
|
|
785
|
+
(`g2`), direct grants (`p`) - plus the shared structural trees (`role_inherits` → `g`,
|
|
766
786
|
`domain_inherits` → `g3`, `resource_inherits` → `g4`, `action_inherits` → `g5`).
|
|
767
787
|
2. **Role closure:** `expandRoleClosure` does a cycle-safe BFS over the `role_inherits` (`g`) edges to
|
|
768
788
|
collect the assigned roles + all transitive parents.
|
|
@@ -898,7 +918,7 @@ Supports these formats:
|
|
|
898
918
|
// String array
|
|
899
919
|
roles: ['admin', 'user']
|
|
900
920
|
|
|
901
|
-
// Object array with identifier (preferred
|
|
921
|
+
// Object array with identifier (preferred - matches AuthorizationRole.identifier)
|
|
902
922
|
roles: [{ id: 1, identifier: '900_admin', priority: 900 }]
|
|
903
923
|
|
|
904
924
|
// Object array with name fallback
|
|
@@ -999,7 +1019,7 @@ buildRouteMiddlewares<RouteConfig extends IAuthRouteConfig>(opts: { configs: Rou
|
|
|
999
1019
|
mws.push(authenticateFn({ strategies, mode }));
|
|
1000
1020
|
}
|
|
1001
1021
|
|
|
1002
|
-
// 2. Authorize middleware (second)
|
|
1022
|
+
// 2. Authorize middleware (second) - supports single or array
|
|
1003
1023
|
if (authorize) {
|
|
1004
1024
|
const specs = Array.isArray(authorize) ? authorize : [authorize];
|
|
1005
1025
|
for (const spec of specs) {
|
|
@@ -1026,7 +1046,7 @@ buildRpcMiddlewares(opts: { configs: IRpcMetadata }): TRpcMiddleware[] {
|
|
|
1026
1046
|
// 1. Authenticate middleware (first)
|
|
1027
1047
|
if (configs.authenticate) { ... }
|
|
1028
1048
|
|
|
1029
|
-
// 2. Authorize middleware (second)
|
|
1049
|
+
// 2. Authorize middleware (second) - same pattern as REST
|
|
1030
1050
|
if (configs.authorize) {
|
|
1031
1051
|
const specs = Array.isArray(configs.authorize) ? configs.authorize : [configs.authorize];
|
|
1032
1052
|
for (const spec of specs) {
|
|
@@ -76,13 +76,13 @@ All error messages from the authorization module, organized by source:
|
|
|
76
76
|
| `[CasbinAuthorizationEnforcer] request.action and request.resource are required.` | 500 | `evaluate` |
|
|
77
77
|
| `[CasbinAuthorizationEnforcer] keyFn returned an empty cache key.` | 400 | `buildRules`/cache management (via `resolveCacheKey`) |
|
|
78
78
|
| <code v-pre>[CasbinAuthorizationEnforcer] cached.options.expiresIn must be >= 10000 (ms) | Received: {{value}}</code> | 500 | `configure` (via `validateExpiresIn`) |
|
|
79
|
-
| <code v-pre>[CasbinAuthorizationEnforcer] Matcher smoke test failed at warmup
|
|
79
|
+
| <code v-pre>[CasbinAuthorizationEnforcer] Matcher smoke test failed at warmup - ...</code> | 500 | `configure` (via `assertMatcherCompilesSync`) |
|
|
80
80
|
| <code v-pre>[resolveDomainMatchingFn] Unsupported func: {{name}} | Valids: [...]</code> | 500 | `configure` (via `registerMatchers`) |
|
|
81
81
|
| <code v-pre>[registerMatchers] Role definition "{{name}}" is not declared in the Casbin model. ...</code> | 500 | `configure` (via `registerMatchers`, only when `domainMatching` is set) |
|
|
82
82
|
| <code v-pre>[resolveModel] Invalid model.driver | Valids: [file, text]</code> | 500 | `configure` (via `resolveModel`) |
|
|
83
83
|
| `[CasbinAuthorizationEnforcer] Cache management requires the redis cache driver, but caching is disabled.` | 500 | `invalidateUserCache`/`rebuildUserCache` (via `requireRedisCache`) |
|
|
84
84
|
|
|
85
|
-
> The `Invalid cached.driver` errors were removed
|
|
85
|
+
> The `Invalid cached.driver` errors were removed - `cached` is now a typed union
|
|
86
86
|
> (`{ use: false } | { use: true, driver: 'redis', ... }`), so an invalid driver is a compile-time
|
|
87
87
|
> error, not a runtime one.
|
|
88
88
|
|
|
@@ -93,9 +93,9 @@ All error messages from the authorization module, organized by source:
|
|
|
93
93
|
| `[CasbinAuthorizationEnforcer] keyFn returned an empty cache key.` | 400 | `resolveCacheKey` (read + cache-management paths) |
|
|
94
94
|
| `[extractUserLines] Adapter does not support loadFilteredPolicy.` | 500 | `extractUserLines` |
|
|
95
95
|
| `[loadPolicyLinesIntoModel] Not configured. Call configure() first.` | 500 | `loadPolicyLinesIntoModel` |
|
|
96
|
-
| `[CasbinAuthorizationEnforcer] Cached payload is not an array of policy lines.` |
|
|
96
|
+
| `[CasbinAuthorizationEnforcer] Cached payload is not an array of policy lines.` | - (logged, not thrown) | `parseCachedPolicyLines` - corrupt entry is discarded + refetched |
|
|
97
97
|
|
|
98
|
-
> A corrupted Redis cache entry does **not** raise an error
|
|
98
|
+
> A corrupted Redis cache entry does **not** raise an error - it is logged and discarded, and the
|
|
99
99
|
> lines are refetched from the adapter (the request never 500s on cache corruption).
|
|
100
100
|
|
|
101
101
|
### Registry Errors (AuthorizationEnforcerRegistry)
|
|
@@ -360,7 +360,7 @@ keyFn: ({ user }) => `authz:policies:${user.principalType}:${user.userId}`,
|
|
|
360
360
|
|
|
361
361
|
**Fix:** Use `CasbinEnforcerModelDrivers.FILE` (`'file'`) or `CasbinEnforcerModelDrivers.TEXT` (`'text'`).
|
|
362
362
|
|
|
363
|
-
> There is no longer an `Invalid cached.driver` runtime error
|
|
363
|
+
> There is no longer an `Invalid cached.driver` runtime error - `cached` is a typed union, so an
|
|
364
364
|
> unsupported cache driver is caught at compile time. Caching is **Redis-only**.
|
|
365
365
|
|
|
366
366
|
## Common Patterns
|
|
@@ -389,7 +389,7 @@ Check that rules are being cached correctly. The middleware caches on `Authoriza
|
|
|
389
389
|
|
|
390
390
|
### Redis Cache Not Working
|
|
391
391
|
|
|
392
|
-
1. **Check Redis connection** -- verify `
|
|
392
|
+
1. **Check Redis connection** -- verify the `AbstractRedisHelper` subclass (`RedisSingleHelper`, `RedisClusterHelper`, or `RedisSentinelHelper`) is properly connected
|
|
393
393
|
2. **Check keyFn** -- ensure it returns a unique, non-empty key per user
|
|
394
394
|
3. **Check expiresIn** -- must be >= 10,000 ms (`MIN_EXPIRES_IN`)
|
|
395
395
|
4. **Verify cache hit** -- check logs for `"Loaded CACHED Policies"` vs `"Loaded ADAPTER + CACHED Policies"`
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
| **authorize** | Standalone function wrapping `AuthorizationProvider.value()` |
|
|
22
22
|
| **AuthorizationRole** | Value object for role identity with priority-based comparison |
|
|
23
23
|
| **BaseFilteredAdapter** | Thin abstract casbin `FilteredAdapter` (datasource plumbing + `loadLines`); subclasses implement only `loadFilteredPolicy` |
|
|
24
|
-
| **ScopedCasbinAdapter** | Generic read-only `FilteredAdapter` for the scoped RBAC model
|
|
24
|
+
| **ScopedCasbinAdapter** | Generic read-only `FilteredAdapter` for the scoped RBAC model - reads one principal's edges + the shared hierarchy from a single `PolicyDefinition` table |
|
|
25
25
|
| **AbstractAuthRegistry** | Shared base class for authentication strategy registry and authorization enforcer registry |
|
|
26
26
|
|
|
27
27
|
### Authorization Flow (7 Steps)
|
|
@@ -147,7 +147,7 @@ Built-in Casbin matching functions selectable for `ICasbinEnforcerOptions.domain
|
|
|
147
147
|
`CasbinDomainMatchingFunctions.SCHEME_SET` contains all valid values. `CasbinDomainMatchingFunctions.isValid(input)` checks membership. Companion type: `TCasbinDomainMatchingFunction`.
|
|
148
148
|
|
|
149
149
|
> [!IMPORTANT]
|
|
150
|
-
> The function is applied as `fn(requestDomain, policyDomain)`
|
|
150
|
+
> The function is applied as `fn(requestDomain, policyDomain)` - the wildcard must live on the **stored/policy** side. With `keyMatch`: `keyMatch("Merchant_X", "*") === true`, `keyMatch("Merchant_X", "Merchant_X") === true`, `keyMatch("Merchant_X", "Merchant_Y") === false`. Store only `*` or exact domain values (never partial patterns like `Merchant_*`) to keep tenant isolation guaranteed.
|
|
151
151
|
|
|
152
152
|
### Casbin Rule Variants
|
|
153
153
|
|
|
@@ -264,6 +264,7 @@ import type {
|
|
|
264
264
|
|
|
265
265
|
// Adapter types
|
|
266
266
|
ICasbinPolicyFilter,
|
|
267
|
+
ICasbinPolicySource,
|
|
267
268
|
IScopedCasbinEntities,
|
|
268
269
|
IScopedCasbinPolicyFilter,
|
|
269
270
|
|
|
@@ -345,7 +346,7 @@ import {
|
|
|
345
346
|
CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
346
347
|
} from '@venizia/ignis';
|
|
347
348
|
|
|
348
|
-
// The generic scoped adapter
|
|
349
|
+
// The generic scoped adapter - reads one principal's edges + the shared hierarchy from a single
|
|
349
350
|
// PolicyDefinition edge table. No subclassing; configure it with IScopedCasbinEntities.
|
|
350
351
|
const adapter = new ScopedCasbinAdapter({
|
|
351
352
|
dataSource,
|
|
@@ -381,7 +382,7 @@ AuthorizationEnforcerRegistry.getInstance().register({
|
|
|
381
382
|
},
|
|
382
383
|
},
|
|
383
384
|
// poolSize / poolAcquireTimeoutMs are optional (defaults 16 / 5000ms).
|
|
384
|
-
// In scoped mode you do NOT pass domainMatching or normalizePayloadFn
|
|
385
|
+
// In scoped mode you do NOT pass domainMatching or normalizePayloadFn - the request domain
|
|
385
386
|
// is supplied by the provider's domain resolver (see "Domain scoping" in usage.md).
|
|
386
387
|
},
|
|
387
388
|
}],
|
|
@@ -493,7 +494,7 @@ Casbin-specific options, provided per-enforcer via `AuthorizationEnforcerRegistr
|
|
|
493
494
|
| `poolSize` | `number` | `16` | Number of pooled enforcers (each request enforces on its own) |
|
|
494
495
|
| `poolAcquireTimeoutMs` | `number` | `5000` | Max ms to wait for a free pooled enforcer before failing closed |
|
|
495
496
|
| `normalizePayloadFn` | `(opts) => { subject, resource, action, domain? }` | -- | (Non-scoped/custom) normalize subject/resource/action before evaluation |
|
|
496
|
-
| `domainMatching` | `{ roleDefinition: string; fn: TCasbinDomainMatchingFunction }` | -- | (Non-scoped) opt-in domain matching function on a role definition. **Not needed when `isScoped: true`**
|
|
497
|
+
| `domainMatching` | `{ roleDefinition: string; fn: TCasbinDomainMatchingFunction }` | -- | (Non-scoped) opt-in domain matching function on a role definition. **Not needed when `isScoped: true`** - the scoped model registers its matchers automatically |
|
|
497
498
|
|
|
498
499
|
```typescript
|
|
499
500
|
interface ICasbinEnforcerOptions<
|
|
@@ -532,7 +533,7 @@ interface ICasbinEnforcerOptions<
|
|
|
532
533
|
};
|
|
533
534
|
|
|
534
535
|
// Non-scoped only. Registers a Casbin domain matching function on the named role definition.
|
|
535
|
-
// When isScoped is true, the scoped model registers its own matchers
|
|
536
|
+
// When isScoped is true, the scoped model registers its own matchers - do not set this.
|
|
536
537
|
domainMatching?: {
|
|
537
538
|
roleDefinition: string; // e.g. 'g'
|
|
538
539
|
fn: TCasbinDomainMatchingFunction;
|
|
@@ -548,14 +549,14 @@ interface ICasbinEnforcerOptions<
|
|
|
548
549
|
The `cached` field is a discriminated union. **Caching is Redis-only** (the in-memory driver was removed):
|
|
549
550
|
|
|
550
551
|
```typescript
|
|
551
|
-
// No caching
|
|
552
|
+
// No caching - every request rebuilds the user's policy from the datasource.
|
|
552
553
|
interface { use: false }
|
|
553
554
|
|
|
554
555
|
// Redis cache (store/retrieve the user's policy lines from Redis, TTL via PX).
|
|
555
556
|
interface ICasbinEnforcerCachedRedis {
|
|
556
557
|
driver: 'redis';
|
|
557
558
|
options: {
|
|
558
|
-
connection:
|
|
559
|
+
connection: IRedisHelper;
|
|
559
560
|
expiresIn: number;
|
|
560
561
|
keyFn: (opts: { user: IAuthorizationUser }) => ValueOrPromise<string>;
|
|
561
562
|
};
|
|
@@ -743,7 +744,7 @@ buildRouteMiddlewares<RouteConfig extends IAuthRouteConfig>(opts: { configs: Rou
|
|
|
743
744
|
mws.push(authenticateFn({ strategies, mode }));
|
|
744
745
|
}
|
|
745
746
|
|
|
746
|
-
// 2. Authorize middleware (second)
|
|
747
|
+
// 2. Authorize middleware (second) - supports single or array
|
|
747
748
|
if (authorize) {
|
|
748
749
|
const specs = Array.isArray(authorize) ? authorize : [authorize];
|
|
749
750
|
for (const spec of specs) {
|
|
@@ -771,7 +772,7 @@ buildRpcMiddlewares(opts: { configs: IRpcMetadata }): TRpcMiddleware[] {
|
|
|
771
772
|
// 1. Authenticate middleware
|
|
772
773
|
if (configs.authenticate) { ... }
|
|
773
774
|
|
|
774
|
-
// 2. Authorize middleware
|
|
775
|
+
// 2. Authorize middleware - same pattern as REST
|
|
775
776
|
if (configs.authorize) {
|
|
776
777
|
const specs = Array.isArray(configs.authorize) ? configs.authorize : [configs.authorize];
|
|
777
778
|
for (const spec of specs) {
|