@frontmcp/skills 1.0.4 → 1.1.0-beta.1
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/catalog/frontmcp-authorities/SKILL.md +272 -0
- package/catalog/frontmcp-authorities/references/authority-profiles.md +262 -0
- package/catalog/frontmcp-authorities/references/claims-mapping.md +266 -0
- package/catalog/frontmcp-authorities/references/custom-evaluators.md +420 -0
- package/catalog/frontmcp-authorities/references/rbac-abac-rebac.md +391 -0
- package/catalog/frontmcp-channels/SKILL.md +122 -0
- package/catalog/frontmcp-channels/examples/channel-sources/agent-notify.md +70 -0
- package/catalog/frontmcp-channels/examples/channel-sources/app-errors.md +71 -0
- package/catalog/frontmcp-channels/examples/channel-sources/file-watcher.md +102 -0
- package/catalog/frontmcp-channels/examples/channel-sources/job-completion.md +79 -0
- package/catalog/frontmcp-channels/examples/channel-sources/replay-buffer.md +106 -0
- package/catalog/frontmcp-channels/examples/channel-sources/service-connector.md +136 -0
- package/catalog/frontmcp-channels/examples/channel-sources/webhook-github.md +85 -0
- package/catalog/frontmcp-channels/examples/channel-two-way/whatsapp-bridge.md +133 -0
- package/catalog/frontmcp-channels/references/channel-sources.md +214 -0
- package/catalog/frontmcp-channels/references/channel-two-way.md +195 -0
- package/catalog/frontmcp-config/SKILL.md +20 -18
- package/catalog/frontmcp-config/examples/configure-auth/multi-app-auth.md +1 -2
- package/catalog/frontmcp-config/examples/configure-auth/public-mode-setup.md +1 -2
- package/catalog/frontmcp-config/examples/configure-auth/remote-oauth-with-vault.md +1 -2
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-self-signed-tokens.md +1 -2
- package/catalog/frontmcp-config/examples/configure-auth-modes/remote-enterprise-oauth.md +1 -2
- package/catalog/frontmcp-config/examples/configure-auth-modes/transparent-jwt-validation.md +1 -2
- package/catalog/frontmcp-config/examples/configure-deployment-targets/distributed-ha-config.md +121 -0
- package/catalog/frontmcp-config/examples/configure-deployment-targets/json-schema-ide-support.md +64 -0
- package/catalog/frontmcp-config/examples/configure-deployment-targets/multi-target-with-security.md +113 -0
- package/catalog/frontmcp-config/examples/configure-elicitation/basic-confirmation-gate.md +1 -2
- package/catalog/frontmcp-config/examples/configure-elicitation/distributed-elicitation-redis.md +1 -2
- package/catalog/frontmcp-config/examples/configure-http/entry-path-reverse-proxy.md +1 -2
- package/catalog/frontmcp-config/examples/configure-http/unix-socket-local.md +1 -2
- package/catalog/frontmcp-config/examples/configure-security-headers/csp-report-only.md +69 -0
- package/catalog/frontmcp-config/examples/configure-security-headers/full-production-headers.md +91 -0
- package/catalog/frontmcp-config/examples/configure-throttle/distributed-redis-throttle.md +1 -2
- package/catalog/frontmcp-config/examples/configure-throttle/per-tool-rate-limit.md +1 -2
- package/catalog/frontmcp-config/examples/configure-throttle/server-level-rate-limit.md +1 -2
- package/catalog/frontmcp-config/examples/configure-transport/custom-protocol-flags.md +1 -2
- package/catalog/frontmcp-config/examples/configure-transport/distributed-sessions-redis.md +1 -2
- package/catalog/frontmcp-config/examples/configure-transport/stateless-serverless.md +1 -2
- package/catalog/frontmcp-config/examples/configure-transport-protocol-presets/legacy-preset-nodejs.md +1 -2
- package/catalog/frontmcp-config/examples/configure-transport-protocol-presets/stateless-api-serverless.md +1 -2
- package/catalog/frontmcp-config/references/configure-deployment-targets.md +214 -0
- package/catalog/frontmcp-config/references/configure-elicitation.md +1 -2
- package/catalog/frontmcp-config/references/configure-security-headers.md +198 -0
- package/catalog/frontmcp-deployment/SKILL.md +1 -0
- package/catalog/frontmcp-deployment/examples/build-for-cli/cli-binary-build.md +1 -2
- package/catalog/frontmcp-deployment/examples/build-for-cli/unix-socket-daemon.md +1 -2
- package/catalog/frontmcp-deployment/examples/build-for-mcpb/mcpb-bundle-build.md +117 -0
- package/catalog/frontmcp-deployment/examples/build-for-sdk/connect-openai.md +1 -3
- package/catalog/frontmcp-deployment/examples/build-for-sdk/create-flat-config.md +1 -2
- package/catalog/frontmcp-deployment/examples/build-for-sdk/multi-platform-connect.md +3 -3
- package/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/basic-worker-deploy.md +1 -2
- package/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-custom-domain.md +1 -2
- package/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-with-kv-storage.md +1 -2
- package/catalog/frontmcp-deployment/examples/deploy-to-lambda/lambda-handler-with-cors.md +1 -2
- package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md +1 -2
- package/catalog/frontmcp-deployment/examples/mcp-client-integration/http-remote.md +106 -0
- package/catalog/frontmcp-deployment/examples/mcp-client-integration/stdio-binary-with-env.md +107 -0
- package/catalog/frontmcp-deployment/examples/mcp-client-integration/stdio-npx.md +89 -0
- package/catalog/frontmcp-deployment/references/build-for-mcpb.md +209 -0
- package/catalog/frontmcp-deployment/references/build-for-sdk.md +1 -2
- package/catalog/frontmcp-deployment/references/mcp-client-integration.md +225 -0
- package/catalog/frontmcp-development/examples/create-agent/basic-agent-with-tools.md +3 -6
- package/catalog/frontmcp-development/examples/create-agent/custom-multi-pass-agent.md +1 -2
- package/catalog/frontmcp-development/examples/create-agent/nested-agents-with-swarm.md +2 -4
- package/catalog/frontmcp-development/examples/create-agent-llm-config/anthropic-config.md +1 -2
- package/catalog/frontmcp-development/examples/create-agent-llm-config/openai-config.md +1 -2
- package/catalog/frontmcp-development/examples/create-job/basic-report-job.md +1 -2
- package/catalog/frontmcp-development/examples/create-job/job-with-permissions.md +2 -3
- package/catalog/frontmcp-development/examples/create-job/job-with-retry.md +1 -2
- package/catalog/frontmcp-development/examples/create-plugin-hooks/tool-level-hooks-and-stage-replacement.md +2 -5
- package/catalog/frontmcp-development/examples/create-provider/basic-database-provider.md +4 -3
- package/catalog/frontmcp-development/examples/create-skill-with-tools/directory-skill-with-tools.md +2 -3
- package/catalog/frontmcp-development/examples/create-tool/basic-class-tool.md +1 -2
- package/catalog/frontmcp-development/examples/create-tool/tool-with-di-and-errors.md +2 -2
- package/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md +1 -2
- package/catalog/frontmcp-development/examples/create-tool-annotations/destructive-delete-tool.md +2 -4
- package/catalog/frontmcp-development/examples/create-tool-annotations/readonly-query-tool.md +1 -2
- package/catalog/frontmcp-development/examples/create-tool-output-schema-types/primitive-and-media-outputs.md +3 -6
- package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-raw-shape-output.md +1 -2
- package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-schema-advanced-output.md +2 -4
- package/catalog/frontmcp-development/examples/decorators-guide/agent-skill-job-workflow.md +3 -5
- package/catalog/frontmcp-development/examples/decorators-guide/basic-server-with-app-and-tools.md +5 -5
- package/catalog/frontmcp-development/examples/decorators-guide/multi-app-with-plugins-and-providers.md +4 -6
- package/catalog/frontmcp-development/examples/official-plugins/cache-and-feature-flags.md +3 -5
- package/catalog/frontmcp-development/examples/official-plugins/production-multi-plugin-setup.md +4 -5
- package/catalog/frontmcp-development/examples/official-plugins/remember-plugin-session-memory.md +3 -5
- package/catalog/frontmcp-development/references/create-agent.md +4 -7
- package/catalog/frontmcp-development/references/create-job.md +3 -6
- package/catalog/frontmcp-development/references/create-plugin-hooks.md +12 -16
- package/catalog/frontmcp-development/references/create-skill-with-tools.md +2 -3
- package/catalog/frontmcp-development/references/create-tool.md +93 -23
- package/catalog/frontmcp-development/references/create-workflow.md +2 -3
- package/catalog/frontmcp-development/references/decorators-guide.md +32 -36
- package/catalog/frontmcp-extensibility/examples/vectoriadb/product-catalog-search.md +4 -4
- package/catalog/frontmcp-extensibility/examples/vectoriadb/semantic-search-with-persistence.md +4 -4
- package/catalog/frontmcp-extensibility/examples/vectoriadb/tfidf-keyword-search.md +4 -3
- package/catalog/frontmcp-guides/SKILL.md +3 -3
- package/catalog/frontmcp-guides/examples/example-knowledge-base/agent-and-plugin.md +4 -5
- package/catalog/frontmcp-guides/examples/example-knowledge-base/vector-search-and-resources.md +4 -3
- package/catalog/frontmcp-guides/examples/example-task-manager/auth-and-crud-tools.md +4 -4
- package/catalog/frontmcp-guides/examples/example-weather-api/weather-tool-with-schemas.md +1 -2
- package/catalog/frontmcp-guides/references/example-knowledge-base.md +22 -17
- package/catalog/frontmcp-guides/references/example-task-manager.md +16 -11
- package/catalog/frontmcp-guides/references/example-weather-api.md +6 -3
- package/catalog/frontmcp-observability/examples/telemetry-api/tool-custom-spans.md +2 -3
- package/catalog/frontmcp-observability/examples/tracing-setup/basic-tracing.md +4 -3
- package/catalog/frontmcp-observability/references/telemetry-api.md +2 -3
- package/catalog/frontmcp-production-readiness/examples/common-checklist/observability-setup.md +1 -2
- package/catalog/frontmcp-production-readiness/examples/common-checklist/security-hardening.md +3 -4
- package/catalog/frontmcp-production-readiness/examples/distributed-ha/ha-kubernetes-3-replicas.md +229 -0
- package/catalog/frontmcp-production-readiness/examples/production-browser/cross-platform-crypto.md +2 -3
- package/catalog/frontmcp-production-readiness/examples/production-cli-binary/stdio-transport-error-handling.md +1 -2
- package/catalog/frontmcp-production-readiness/examples/production-cloudflare/durable-objects-state.md +2 -4
- package/catalog/frontmcp-production-readiness/examples/production-cloudflare/workers-runtime-constraints.md +2 -3
- package/catalog/frontmcp-production-readiness/examples/production-lambda/cold-start-connection-reuse.md +3 -2
- package/catalog/frontmcp-production-readiness/examples/production-vercel/cold-start-optimization.md +2 -2
- package/catalog/frontmcp-production-readiness/examples/production-vercel/stateless-serverless-design.md +3 -3
- package/catalog/frontmcp-production-readiness/references/distributed-ha.md +194 -0
- package/catalog/frontmcp-setup/SKILL.md +11 -11
- package/catalog/frontmcp-setup/examples/project-structure-standalone/feature-folder-organization.md +5 -3
- package/catalog/frontmcp-setup/examples/project-structure-standalone/minimal-standalone-layout.md +4 -2
- package/catalog/frontmcp-setup/examples/setup-project/basic-node-server.md +4 -2
- package/catalog/frontmcp-setup/examples/setup-project/vercel-serverless-server.md +4 -2
- package/catalog/frontmcp-setup/examples/setup-redis/hybrid-vercel-kv-with-pubsub.md +8 -7
- package/catalog/frontmcp-setup/references/setup-project.md +10 -9
- package/catalog/frontmcp-setup/references/setup-redis.md +19 -16
- package/catalog/frontmcp-testing/examples/test-direct-client/basic-create-test.md +1 -3
- package/catalog/frontmcp-testing/examples/test-direct-client/openai-claude-format-test.md +1 -3
- package/catalog/frontmcp-testing/examples/test-tool-unit/schema-validation-test.md +2 -2
- package/catalog/frontmcp-testing/references/test-direct-client.md +1 -3
- package/catalog/frontmcp-testing/references/test-tool-unit.md +2 -2
- package/catalog/skills-manifest.json +325 -3
- package/package.json +1 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claims-mapping
|
|
3
|
+
description: JWT claims mapping configuration per identity provider (Auth0, Keycloak, Okta, Cognito, Frontegg) for the authorities system
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# JWT Claims Mapping
|
|
7
|
+
|
|
8
|
+
The `claimsMapping` field in `@FrontMcp({ authorities: { claimsMapping } })` tells the authorities engine where to find roles, permissions, tenant ID, and user ID within the decoded JWT payload. Every identity provider structures its tokens differently, so this mapping is required for correct policy evaluation.
|
|
9
|
+
|
|
10
|
+
## How Claims Mapping Works
|
|
11
|
+
|
|
12
|
+
The engine receives `authInfo` on each request, which contains the decoded JWT claims. The `claimsMapping` fields are dot-paths resolved against these claims using `resolveDotPath()`. The resolver first tries a direct key lookup (for keys containing dots, such as Auth0 namespaced claims), then falls back to dot-separated path traversal.
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
interface AuthoritiesClaimsMapping {
|
|
16
|
+
/** Dot-path to roles array in JWT claims */
|
|
17
|
+
roles?: string;
|
|
18
|
+
/** Dot-path to permissions array/string in JWT claims */
|
|
19
|
+
permissions?: string;
|
|
20
|
+
/** Dot-path to tenant/org ID in JWT claims */
|
|
21
|
+
tenantId?: string;
|
|
22
|
+
/** Dot-path to user ID in JWT claims (default: 'sub') */
|
|
23
|
+
userId?: string;
|
|
24
|
+
/** Extensible: additional custom claim mappings */
|
|
25
|
+
[key: string]: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Auth0
|
|
30
|
+
|
|
31
|
+
Auth0 stores roles in a custom namespaced claim (you define the namespace) and permissions in a flat `permissions` array. The org ID (if using Auth0 Organizations) is in `org_id`.
|
|
32
|
+
|
|
33
|
+
**Sample JWT payload:**
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"sub": "auth0|abc123",
|
|
38
|
+
"https://myapp.com/roles": ["admin", "editor"],
|
|
39
|
+
"permissions": ["users:read", "users:write", "content:publish"],
|
|
40
|
+
"org_id": "org_def456",
|
|
41
|
+
"aud": "https://api.myapp.com",
|
|
42
|
+
"iss": "https://myapp.auth0.com/"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Claims mapping:**
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// In @FrontMcp({ authorities: { ... } })
|
|
50
|
+
authorities: {
|
|
51
|
+
claimsMapping: {
|
|
52
|
+
roles: 'https://myapp.com/roles',
|
|
53
|
+
permissions: 'permissions',
|
|
54
|
+
tenantId: 'org_id',
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Notes:**
|
|
60
|
+
|
|
61
|
+
- The roles claim namespace (`https://myapp.com/roles`) is configured in an Auth0 Action or Rule. It must be a fully qualified URL.
|
|
62
|
+
- `resolveDotPath` handles the dotted namespace key via direct key lookup, so `https://myapp.com/roles` works correctly.
|
|
63
|
+
- Permissions appear only if the API has RBAC enabled in Auth0 dashboard.
|
|
64
|
+
|
|
65
|
+
## Keycloak
|
|
66
|
+
|
|
67
|
+
Keycloak nests realm roles under `realm_access.roles` and client-specific roles under `resource_access.<client-id>.roles`. Permissions are typically modeled as client roles or fine-grained UMA permissions.
|
|
68
|
+
|
|
69
|
+
**Sample JWT payload:**
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"sub": "f1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
74
|
+
"realm_access": {
|
|
75
|
+
"roles": ["admin", "user", "offline_access"]
|
|
76
|
+
},
|
|
77
|
+
"resource_access": {
|
|
78
|
+
"my-client": {
|
|
79
|
+
"roles": ["content:write", "content:publish"]
|
|
80
|
+
},
|
|
81
|
+
"account": {
|
|
82
|
+
"roles": ["manage-account"]
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"preferred_username": "jane.doe",
|
|
86
|
+
"azp": "my-client",
|
|
87
|
+
"iss": "https://keycloak.example.com/realms/myrealm"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Claims mapping:**
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// In @FrontMcp({ authorities: { ... } })
|
|
95
|
+
authorities: {
|
|
96
|
+
claimsMapping: {
|
|
97
|
+
roles: 'realm_access.roles',
|
|
98
|
+
permissions: 'resource_access.my-client.roles',
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Notes:**
|
|
104
|
+
|
|
105
|
+
- Replace `my-client` with your actual Keycloak client ID.
|
|
106
|
+
- Keycloak adds system roles like `offline_access` and `uma_authorization` to `realm_access.roles`. You may want to use `all` instead of `any` in RBAC policies to avoid matching on these.
|
|
107
|
+
- For multi-tenant Keycloak setups, tenant ID is often a custom claim added via a protocol mapper.
|
|
108
|
+
|
|
109
|
+
## Okta
|
|
110
|
+
|
|
111
|
+
Okta uses `groups` for role-like claims and `scp` (or `scope`) for permission-like claims. Groups must be explicitly included in the token via an Authorization Server claim configuration.
|
|
112
|
+
|
|
113
|
+
**Sample JWT payload:**
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"sub": "00u1a2b3c4d5e6f7g8h9",
|
|
118
|
+
"groups": ["Admin", "Engineering", "Everyone"],
|
|
119
|
+
"scp": ["openid", "profile", "users.read", "users.write"],
|
|
120
|
+
"cid": "0oa1b2c3d4e5f6g7h8",
|
|
121
|
+
"uid": "00u1a2b3c4d5e6f7g8h9",
|
|
122
|
+
"iss": "https://myorg.okta.com/oauth2/default"
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Claims mapping:**
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// In @FrontMcp({ authorities: { ... } })
|
|
130
|
+
authorities: {
|
|
131
|
+
claimsMapping: {
|
|
132
|
+
roles: 'groups',
|
|
133
|
+
permissions: 'scp',
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Notes:**
|
|
139
|
+
|
|
140
|
+
- Groups must be added as a claim to the authorization server. Go to Security > API > Authorization Servers > Claims > Add Claim with value type "Groups" and filter "Matches regex `.*`".
|
|
141
|
+
- `scp` is an array of scope strings. If your Okta config uses `scope` (singular), adjust the path accordingly.
|
|
142
|
+
- Okta does not have a built-in org/tenant claim. For multi-tenant, add a custom claim via a token hook.
|
|
143
|
+
|
|
144
|
+
## Amazon Cognito
|
|
145
|
+
|
|
146
|
+
Cognito uses `cognito:groups` for groups and `scope` (space-separated string) for OAuth scopes. Custom attributes use the `custom:` prefix.
|
|
147
|
+
|
|
148
|
+
**Sample JWT payload:**
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
153
|
+
"cognito:groups": ["Admins", "PowerUsers"],
|
|
154
|
+
"scope": "openid profile aws.cognito.signin.user.admin",
|
|
155
|
+
"custom:tenantId": "tenant-abc",
|
|
156
|
+
"client_id": "1a2b3c4d5e6f7g8h9i0j",
|
|
157
|
+
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_AbCdEfG"
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Claims mapping:**
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// In @FrontMcp({ authorities: { ... } })
|
|
165
|
+
authorities: {
|
|
166
|
+
claimsMapping: {
|
|
167
|
+
roles: 'cognito:groups',
|
|
168
|
+
permissions: 'scope',
|
|
169
|
+
tenantId: 'custom:tenantId',
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Notes:**
|
|
175
|
+
|
|
176
|
+
- `scope` in Cognito is a space-separated string, not an array. The authorities engine handles this automatically via `toStringArray()`, which splits space-separated strings.
|
|
177
|
+
- `cognito:groups` contains dots in the key name. `resolveDotPath` handles this via direct key lookup.
|
|
178
|
+
- Custom attributes require the `custom:` prefix in both the claim path and the User Pool schema.
|
|
179
|
+
|
|
180
|
+
## Frontegg
|
|
181
|
+
|
|
182
|
+
Frontegg places roles and permissions as flat arrays at the top level. Tenant ID is in `tenantId`. The token also includes `tenantIds` (array of all tenant memberships) for multi-tenant scenarios.
|
|
183
|
+
|
|
184
|
+
**Sample JWT payload:**
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"sub": "user-uuid-123",
|
|
189
|
+
"roles": ["Admin", "ReadOnly"],
|
|
190
|
+
"permissions": ["fe.users.read", "fe.users.write", "fe.tenant.admin"],
|
|
191
|
+
"tenantId": "tenant-uuid-456",
|
|
192
|
+
"tenantIds": ["tenant-uuid-456", "tenant-uuid-789"],
|
|
193
|
+
"email": "jane@example.com",
|
|
194
|
+
"iss": "https://app-abc.frontegg.com"
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Claims mapping:**
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// In @FrontMcp({ authorities: { ... } })
|
|
202
|
+
authorities: {
|
|
203
|
+
claimsMapping: {
|
|
204
|
+
roles: 'roles',
|
|
205
|
+
permissions: 'permissions',
|
|
206
|
+
tenantId: 'tenantId',
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Notes:**
|
|
212
|
+
|
|
213
|
+
- Frontegg tokens have the simplest structure since roles and permissions are flat top-level arrays.
|
|
214
|
+
- The default fallback behavior (no `claimsMapping`) already looks for `roles` and `permissions` at the top level, so Frontegg often works without explicit mapping.
|
|
215
|
+
- For cross-tenant operations, use `tenantIds` (array) instead of `tenantId` (single active tenant).
|
|
216
|
+
|
|
217
|
+
## Custom IdP / claimsResolver
|
|
218
|
+
|
|
219
|
+
If your JWT structure does not fit the dot-path model, use `claimsResolver` instead of `claimsMapping`. This gives full programmatic control over claim extraction.
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// In @FrontMcp({ authorities: { ... } })
|
|
223
|
+
authorities: {
|
|
224
|
+
claimsResolver: (authInfo) => {
|
|
225
|
+
const claims = authInfo?.extra?.authorization?.claims ?? {};
|
|
226
|
+
const user = authInfo?.user ?? {};
|
|
227
|
+
|
|
228
|
+
// Custom extraction logic
|
|
229
|
+
const roles = (claims['x-custom-roles'] as string || '').split(',').filter(Boolean);
|
|
230
|
+
const permissions = Array.isArray(claims['x-permissions'])
|
|
231
|
+
? claims['x-permissions']
|
|
232
|
+
: [];
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
roles,
|
|
236
|
+
permissions,
|
|
237
|
+
claims: { ...claims, ...user },
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**When to use `claimsResolver`:**
|
|
244
|
+
|
|
245
|
+
- The token has roles encoded as a comma-separated string or bitmask
|
|
246
|
+
- Roles need to be derived from multiple claim fields
|
|
247
|
+
- Permissions require runtime transformation (e.g., wildcard expansion)
|
|
248
|
+
- The token structure is deeply nested or unconventional
|
|
249
|
+
|
|
250
|
+
`claimsResolver` takes precedence over `claimsMapping` when both are provided.
|
|
251
|
+
|
|
252
|
+
## Quick Reference Table
|
|
253
|
+
|
|
254
|
+
| IdP | Roles Path | Permissions Path | Tenant Path | Notes |
|
|
255
|
+
| -------- | ------------------------- | -------------------------------- | ----------------- | --------------------------------- |
|
|
256
|
+
| Auth0 | `https://myapp.com/roles` | `permissions` | `org_id` | Namespace is developer-defined |
|
|
257
|
+
| Keycloak | `realm_access.roles` | `resource_access.<client>.roles` | Custom claim | Replace `<client>` with client ID |
|
|
258
|
+
| Okta | `groups` | `scp` | Custom claim | Groups must be added as a claim |
|
|
259
|
+
| Cognito | `cognito:groups` | `scope` | `custom:tenantId` | `scope` is space-separated |
|
|
260
|
+
| Frontegg | `roles` | `permissions` | `tenantId` | Works with default fallback |
|
|
261
|
+
|
|
262
|
+
## Reference
|
|
263
|
+
|
|
264
|
+
- Type: `AuthoritiesClaimsMapping` in `libs/auth/src/authorities/authorities.profiles.ts`
|
|
265
|
+
- Context builder: `AuthoritiesContextBuilder` in `libs/auth/src/authorities/authorities.context.ts`
|
|
266
|
+
- Related: `references/authority-profiles.md`, `references/rbac-abac-rebac.md`
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: custom-evaluators
|
|
3
|
+
description: Creating and registering custom authority evaluators for domain-specific authorization logic beyond RBAC, ABAC, and ReBAC
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Custom Evaluators
|
|
7
|
+
|
|
8
|
+
When the built-in RBAC, ABAC, and ReBAC models do not cover your authorization requirements, you can create custom evaluators. Custom evaluators are registered via the `authorities.evaluators` config in `@FrontMcp()` and invoked via the `custom.*` field in policies.
|
|
9
|
+
|
|
10
|
+
## AuthoritiesEvaluator Interface
|
|
11
|
+
|
|
12
|
+
Every custom evaluator must implement the `AuthoritiesEvaluator` interface from `@frontmcp/auth`.
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import type { AuthoritiesEvaluator, AuthoritiesEvaluationContext, AuthoritiesResult } from '@frontmcp/auth';
|
|
16
|
+
|
|
17
|
+
interface AuthoritiesEvaluator {
|
|
18
|
+
/** Evaluator name (must match the key under `custom.*` in policies) */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Evaluate the policy against the context */
|
|
21
|
+
evaluate(policy: unknown, ctx: AuthoritiesEvaluationContext): Promise<AuthoritiesResult>;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The `policy` parameter is whatever value is passed under the evaluator's key in the `custom` field. The `ctx` parameter provides the full evaluation context including user info, input, environment, and the relationship resolver.
|
|
26
|
+
|
|
27
|
+
The return value must be an `AuthoritiesResult`:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
interface AuthoritiesResult {
|
|
31
|
+
/** Whether access was granted */
|
|
32
|
+
granted: boolean;
|
|
33
|
+
/** Human-readable reason for denial */
|
|
34
|
+
deniedBy?: string;
|
|
35
|
+
/** List of policy types that were evaluated (for audit) */
|
|
36
|
+
evaluatedPolicies: string[];
|
|
37
|
+
/** Optional detailed message */
|
|
38
|
+
message?: string;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Registering Custom Evaluators
|
|
43
|
+
|
|
44
|
+
Custom evaluators are registered in the `evaluators` field of the `authorities` config. The key must match the evaluator's `name` and the key used in `custom.*` policies.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { FrontMcp } from '@frontmcp/sdk';
|
|
48
|
+
import { ipAllowListEvaluator } from './evaluators/ip-allow-list';
|
|
49
|
+
import { featureFlagEvaluator } from './evaluators/feature-flag';
|
|
50
|
+
import { timeWindowEvaluator } from './evaluators/time-window';
|
|
51
|
+
|
|
52
|
+
@FrontMcp({
|
|
53
|
+
name: 'my-server',
|
|
54
|
+
authorities: {
|
|
55
|
+
claimsMapping: { roles: 'roles', permissions: 'permissions' },
|
|
56
|
+
profiles: { admin: { roles: { any: ['admin'] } } },
|
|
57
|
+
evaluators: {
|
|
58
|
+
ipAllowList: ipAllowListEvaluator,
|
|
59
|
+
featureFlag: featureFlagEvaluator,
|
|
60
|
+
timeWindow: timeWindowEvaluator,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
export class MyServer {}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Async Guards: DB/Redis Lookups
|
|
68
|
+
|
|
69
|
+
Custom evaluators are **fully async** — use them for database queries, Redis checks, feature flags, or any I/O-bound authorization logic.
|
|
70
|
+
|
|
71
|
+
### Redis Set Membership
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const tenantAllowlistGuard: AuthoritiesEvaluator = {
|
|
75
|
+
name: 'tenantAllowlist',
|
|
76
|
+
evaluate: async (policy, ctx) => {
|
|
77
|
+
const { redisKey } = policy as { redisKey: string };
|
|
78
|
+
const tenantId = ctx.input['tenantId'] as string;
|
|
79
|
+
const isAllowed = await redis.sismember(redisKey, tenantId);
|
|
80
|
+
return {
|
|
81
|
+
granted: isAllowed,
|
|
82
|
+
deniedBy: isAllowed ? undefined : `tenant '${tenantId}' not in allowlist`,
|
|
83
|
+
denial: isAllowed ? undefined : { kind: 'custom', path: 'custom.tenantAllowlist' },
|
|
84
|
+
evaluatedPolicies: ['custom.tenantAllowlist'],
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Database Query
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const activeSubscriptionGuard: AuthoritiesEvaluator = {
|
|
94
|
+
name: 'activeSubscription',
|
|
95
|
+
evaluate: async (_policy, ctx) => {
|
|
96
|
+
const row = await db.query('SELECT active FROM subscriptions WHERE user_id = $1', [ctx.user.sub]);
|
|
97
|
+
const active = row?.active === true;
|
|
98
|
+
return {
|
|
99
|
+
granted: active,
|
|
100
|
+
deniedBy: active ? undefined : 'no active subscription',
|
|
101
|
+
denial: active ? undefined : { kind: 'custom', path: 'custom.activeSubscription' },
|
|
102
|
+
evaluatedPolicies: ['custom.activeSubscription'],
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Feature Flag
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const featureFlagGuard: AuthoritiesEvaluator = {
|
|
112
|
+
name: 'featureFlag',
|
|
113
|
+
evaluate: async (policy, ctx) => {
|
|
114
|
+
const { flag } = policy as { flag: string };
|
|
115
|
+
const enabled = await featureFlagService.isEnabled(flag, { userId: ctx.user.sub });
|
|
116
|
+
return {
|
|
117
|
+
granted: enabled,
|
|
118
|
+
deniedBy: enabled ? undefined : `feature '${flag}' not enabled`,
|
|
119
|
+
denial: enabled ? undefined : { kind: 'custom', path: `custom.featureFlag.${flag}` },
|
|
120
|
+
evaluatedPolicies: [`custom.featureFlag`],
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### When to Use Custom Evaluators vs Hooks
|
|
127
|
+
|
|
128
|
+
| Approach | When to Use |
|
|
129
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
130
|
+
| **Custom evaluator** | Reusable async check across many tools — register once, reference via `custom` field |
|
|
131
|
+
| **`Will('checkEntryAuthorities')` hook** | One-off async check for a specific plugin/app, not tied to a specific tool |
|
|
132
|
+
| **Static `authorities` policy** | Roles, permissions, attributes — no I/O needed |
|
|
133
|
+
|
|
134
|
+
## Using Custom Evaluators in Policies
|
|
135
|
+
|
|
136
|
+
Reference custom evaluators via the `custom` field on any `AuthoritiesPolicyMetadata`. The key under `custom` must match the registered evaluator name.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
@Tool({
|
|
140
|
+
name: 'admin_panel',
|
|
141
|
+
authorities: {
|
|
142
|
+
roles: { any: ['admin'] },
|
|
143
|
+
custom: {
|
|
144
|
+
ipAllowList: { cidr: ['10.0.0.0/8', '172.16.0.0/12'] },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
export default class AdminPanelTool extends ToolContext { ... }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The engine evaluates `custom` evaluators alongside RBAC, ABAC, and ReBAC. They follow the same `operator` semantics (default `'AND'`), so in the example above the user must have the `admin` role AND pass the IP allowlist check.
|
|
152
|
+
|
|
153
|
+
## Example: IP Allowlist Evaluator
|
|
154
|
+
|
|
155
|
+
Restricts access to requests from specific CIDR ranges.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// evaluators/ip-allow-list.ts
|
|
159
|
+
import type { AuthoritiesEvaluator, AuthoritiesEvaluationContext, AuthoritiesResult } from '@frontmcp/auth';
|
|
160
|
+
|
|
161
|
+
interface IpAllowListPolicy {
|
|
162
|
+
cidr: string[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isInCidr(ip: string, cidr: string): boolean {
|
|
166
|
+
// Simplified -- use a library like 'ip-cidr' in production
|
|
167
|
+
const [subnet, bits] = cidr.split('/');
|
|
168
|
+
if (!bits) return ip === subnet;
|
|
169
|
+
// ... full CIDR matching logic
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const ipAllowListEvaluator: AuthoritiesEvaluator = {
|
|
174
|
+
name: 'ipAllowList',
|
|
175
|
+
async evaluate(policy: unknown, ctx: AuthoritiesEvaluationContext): Promise<AuthoritiesResult> {
|
|
176
|
+
const { cidr } = policy as IpAllowListPolicy;
|
|
177
|
+
const remoteIp = ctx.env['remoteIp'] as string | undefined;
|
|
178
|
+
|
|
179
|
+
if (!remoteIp) {
|
|
180
|
+
return {
|
|
181
|
+
granted: false,
|
|
182
|
+
deniedBy: 'custom.ipAllowList: remoteIp not available in env',
|
|
183
|
+
evaluatedPolicies: ['custom.ipAllowList'],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const allowed = cidr.some((c) => isInCidr(remoteIp, c));
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
granted: allowed,
|
|
191
|
+
deniedBy: allowed ? undefined : `custom.ipAllowList: ${remoteIp} not in allowed CIDR ranges`,
|
|
192
|
+
evaluatedPolicies: ['custom.ipAllowList'],
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Usage:**
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
@Tool({
|
|
202
|
+
name: 'internal_admin',
|
|
203
|
+
authorities: {
|
|
204
|
+
custom: {
|
|
205
|
+
ipAllowList: { cidr: ['10.0.0.0/8', '192.168.0.0/16'] },
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
export default class InternalAdminTool extends ToolContext { ... }
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Example: Feature Flag Evaluator
|
|
213
|
+
|
|
214
|
+
Gates access behind a feature flag service.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// evaluators/feature-flag.ts
|
|
218
|
+
import type { AuthoritiesEvaluator, AuthoritiesEvaluationContext, AuthoritiesResult } from '@frontmcp/auth';
|
|
219
|
+
|
|
220
|
+
interface FeatureFlagPolicy {
|
|
221
|
+
flag: string;
|
|
222
|
+
/** If true, the flag must be disabled for access (inverse gate) */
|
|
223
|
+
inverse?: boolean;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Assume a global feature flag client is available
|
|
227
|
+
declare const featureFlags: { isEnabled(flag: string, userId: string): Promise<boolean> };
|
|
228
|
+
|
|
229
|
+
export const featureFlagEvaluator: AuthoritiesEvaluator = {
|
|
230
|
+
name: 'featureFlag',
|
|
231
|
+
async evaluate(policy: unknown, ctx: AuthoritiesEvaluationContext): Promise<AuthoritiesResult> {
|
|
232
|
+
const { flag, inverse } = policy as FeatureFlagPolicy;
|
|
233
|
+
const enabled = await featureFlags.isEnabled(flag, ctx.user.sub);
|
|
234
|
+
const granted = inverse ? !enabled : enabled;
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
granted,
|
|
238
|
+
deniedBy: granted
|
|
239
|
+
? undefined
|
|
240
|
+
: `custom.featureFlag: '${flag}' is ${enabled ? 'enabled' : 'disabled'} (inverse=${!!inverse})`,
|
|
241
|
+
evaluatedPolicies: ['custom.featureFlag'],
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Usage:**
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
@Tool({
|
|
251
|
+
name: 'beta_feature',
|
|
252
|
+
authorities: {
|
|
253
|
+
custom: {
|
|
254
|
+
featureFlag: { flag: 'beta-tools-v2' },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
export default class BetaFeatureTool extends ToolContext { ... }
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Example: Time Window Evaluator
|
|
262
|
+
|
|
263
|
+
Restricts access to specific time windows (e.g., business hours only).
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// evaluators/time-window.ts
|
|
267
|
+
import type { AuthoritiesEvaluator, AuthoritiesEvaluationContext, AuthoritiesResult } from '@frontmcp/auth';
|
|
268
|
+
|
|
269
|
+
interface TimeWindowPolicy {
|
|
270
|
+
/** Allowed days (0=Sunday, 6=Saturday) */
|
|
271
|
+
days: number[];
|
|
272
|
+
/** Start hour (0-23, inclusive) */
|
|
273
|
+
startHour: number;
|
|
274
|
+
/** End hour (0-23, exclusive) */
|
|
275
|
+
endHour: number;
|
|
276
|
+
/** IANA timezone (default: UTC) */
|
|
277
|
+
timezone?: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const timeWindowEvaluator: AuthoritiesEvaluator = {
|
|
281
|
+
name: 'timeWindow',
|
|
282
|
+
async evaluate(policy: unknown, ctx: AuthoritiesEvaluationContext): Promise<AuthoritiesResult> {
|
|
283
|
+
const { days, startHour, endHour, timezone } = policy as TimeWindowPolicy;
|
|
284
|
+
const now = new Date();
|
|
285
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
286
|
+
timeZone: timezone ?? 'UTC',
|
|
287
|
+
hour: 'numeric',
|
|
288
|
+
hour12: false,
|
|
289
|
+
weekday: 'short',
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const parts = formatter.formatToParts(now);
|
|
293
|
+
const hour = parseInt(parts.find((p) => p.type === 'hour')?.value ?? '0', 10);
|
|
294
|
+
// Get day of week in the configured timezone (not local system timezone)
|
|
295
|
+
const dayName = new Intl.DateTimeFormat('en-US', { timeZone: timezone ?? 'UTC', weekday: 'long' }).format(now);
|
|
296
|
+
const dayIndex = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'].indexOf(dayName);
|
|
297
|
+
|
|
298
|
+
const dayAllowed = days.includes(dayIndex);
|
|
299
|
+
const hourAllowed = hour >= startHour && hour < endHour;
|
|
300
|
+
const granted = dayAllowed && hourAllowed;
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
granted,
|
|
304
|
+
deniedBy: granted ? undefined : `custom.timeWindow: current time outside allowed window`,
|
|
305
|
+
evaluatedPolicies: ['custom.timeWindow'],
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Usage:**
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
@Tool({
|
|
315
|
+
name: 'payroll_sync',
|
|
316
|
+
authorities: {
|
|
317
|
+
roles: { any: ['finance'] },
|
|
318
|
+
custom: {
|
|
319
|
+
timeWindow: {
|
|
320
|
+
days: [1, 2, 3, 4, 5], // Monday through Friday
|
|
321
|
+
startHour: 9,
|
|
322
|
+
endHour: 17,
|
|
323
|
+
timezone: 'America/New_York',
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
export default class PayrollSyncTool extends ToolContext { ... }
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Example: Rate Limit Evaluator
|
|
332
|
+
|
|
333
|
+
Denies access when a per-user rate limit is exceeded.
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
// evaluators/rate-limit.ts
|
|
337
|
+
import type { AuthoritiesEvaluator, AuthoritiesEvaluationContext, AuthoritiesResult } from '@frontmcp/auth';
|
|
338
|
+
|
|
339
|
+
interface RateLimitPolicy {
|
|
340
|
+
/** Maximum calls allowed */
|
|
341
|
+
max: number;
|
|
342
|
+
/** Time window in seconds */
|
|
343
|
+
windowSeconds: number;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// In-memory counter (use Redis in production)
|
|
347
|
+
const counters = new Map<string, { count: number; resetAt: number }>();
|
|
348
|
+
|
|
349
|
+
export const rateLimitEvaluator: AuthoritiesEvaluator = {
|
|
350
|
+
name: 'rateLimit',
|
|
351
|
+
async evaluate(policy: unknown, ctx: AuthoritiesEvaluationContext): Promise<AuthoritiesResult> {
|
|
352
|
+
const { max, windowSeconds } = policy as RateLimitPolicy;
|
|
353
|
+
const key = `${ctx.user.sub}`;
|
|
354
|
+
const now = Date.now();
|
|
355
|
+
|
|
356
|
+
let entry = counters.get(key);
|
|
357
|
+
if (!entry || now > entry.resetAt) {
|
|
358
|
+
entry = { count: 0, resetAt: now + windowSeconds * 1000 };
|
|
359
|
+
counters.set(key, entry);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
entry.count++;
|
|
363
|
+
|
|
364
|
+
if (entry.count > max) {
|
|
365
|
+
return {
|
|
366
|
+
granted: false,
|
|
367
|
+
deniedBy: `custom.rateLimit: ${entry.count}/${max} calls in window`,
|
|
368
|
+
evaluatedPolicies: ['custom.rateLimit'],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
granted: true,
|
|
374
|
+
evaluatedPolicies: ['custom.rateLimit'],
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Combining Custom Evaluators with Built-in Models
|
|
381
|
+
|
|
382
|
+
Custom evaluators participate in the same combinator system as RBAC, ABAC, and ReBAC.
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
// Admin from allowed IP during business hours
|
|
386
|
+
authorities: {
|
|
387
|
+
allOf: [
|
|
388
|
+
{ roles: { any: ['admin'] } },
|
|
389
|
+
{ custom: { ipAllowList: { cidr: ['10.0.0.0/8'] } } },
|
|
390
|
+
{ custom: { timeWindow: { days: [1, 2, 3, 4, 5], startHour: 9, endHour: 17 } } },
|
|
391
|
+
],
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Feature flag OR admin bypass
|
|
395
|
+
authorities: {
|
|
396
|
+
anyOf: [
|
|
397
|
+
{ roles: { any: ['admin'] } },
|
|
398
|
+
{ custom: { featureFlag: { flag: 'new-dashboard' } } },
|
|
399
|
+
],
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Best Practices
|
|
404
|
+
|
|
405
|
+
| Practice | Description |
|
|
406
|
+
| --------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
407
|
+
| Always return `evaluatedPolicies` | Include `custom.<name>` in the array for audit trail |
|
|
408
|
+
| Provide descriptive `deniedBy` | Include the evaluator name, the failing condition, and relevant values |
|
|
409
|
+
| Keep evaluators stateless when possible | Use external stores (Redis, database) for state; avoid in-memory maps in production |
|
|
410
|
+
| Type the policy parameter | Cast `policy as YourPolicyType` at the top of `evaluate()` |
|
|
411
|
+
| Handle missing context gracefully | Return denial with a clear message if expected env/input values are absent |
|
|
412
|
+
| Test evaluators in isolation | Each evaluator is a plain object; test `evaluate()` directly with mock contexts |
|
|
413
|
+
| Name evaluators with camelCase | The name must match both the registered key and the `custom.*` policy key |
|
|
414
|
+
|
|
415
|
+
## Reference
|
|
416
|
+
|
|
417
|
+
- Type: `AuthoritiesEvaluator` in `libs/auth/src/authorities/authorities.types.ts`
|
|
418
|
+
- Registry: `AuthoritiesEvaluatorRegistry` in `libs/auth/src/authorities/authorities.registry.ts`
|
|
419
|
+
- Engine dispatch: `evaluateCustom()` in `libs/auth/src/authorities/authorities.engine.ts`
|
|
420
|
+
- Related: `references/authority-profiles.md`, `references/rbac-abac-rebac.md`
|