@frontmcp/skills 1.7.1 → 1.7.2

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.
@@ -67,7 +67,7 @@ Local mode also accepts `allowDefaultPublic` (default `false` — set `true` to
67
67
 
68
68
  > **Public origin (security):** pin `FRONTMCP_PUBLIC_URL` in production. The issuer / resource / OAuth-discovery URLs and the transparent expected audience derive from it rather than from request headers; `X-Forwarded-Host`/`X-Forwarded-Proto` are ignored unless `FRONTMCP_TRUST_PROXY=1` (a trusted proxy that strips client-supplied forwarded headers).
69
69
 
70
- **Progressive / incremental authorization** (opt-in via `incrementalAuth`): when enabled, the minted token carries an `authorized_apps` claim and a `tools/call` for an app NOT in that claim resolves to a `CallToolResult` with `isError: true` and `_meta.code === 'AUTHORIZATION_REQUIRED'` (fields: `authorization_required: true`, `app`, `tool`, `auth_url`, `required_scopes`, `session_mode`, `supports_incremental`). The client declares the initial grant on `/oauth/authorize?…&apps=crm` (omit `apps` to grant all apps) and expands it later via an incremental authorize `…&mode=incremental&app=slack&apps=crm` — the new token's claim is the **union** of the prior apps plus the target (the user identity and already-granted apps are preserved; upstream tokens stay server-side). Without an `incrementalAuth` block, no claim is minted and there is **no** app-level gating (allow-all preserved). `consent` (tool-level) and `incrementalAuth` (app-level) are independent.
70
+ **Progressive / incremental authorization** (opt-in via `incrementalAuth`): when enabled, the minted token carries an `authorized_apps` claim and a `tools/call` for an app NOT in that claim resolves to a `CallToolResult` with `isError: true` and `_meta.code === 'AUTHORIZATION_REQUIRED'` (fields: `authorization_required: true`, `app`, `tool`, `auth_url`, `required_scopes`, `session_mode`, `supports_incremental`). The client declares the initial grant on `/oauth/authorize?…&apps=crm` (omit `apps` to grant all apps) and expands it later by **following the `auth_url` from the failed call** — that URL carries a framework-signed, single-use `ticket` naming the target app and the prior grant, and the new token's claim is the **union** of the prior apps plus the target (the user identity and already-granted apps are preserved; upstream tokens stay server-side). Do NOT hand-assemble `…&mode=incremental&app=slack`: since 1.7.2 (GHSA-2c4g-9c8x-6m8g) an authorize without a valid ticket is an ordinary login and runs the full credential gate, and `/oauth/callback` ignores an `incremental=true` parameter entirely. Without an `incrementalAuth` block, no claim is minted and there is **no** app-level gating (allow-all preserved). `consent` (tool-level) and `incrementalAuth` (app-level) are independent.
71
71
 
72
72
  To collect and verify your own credentials, add a declarative `login` (custom page fields / title / subject strategy) and an `authenticate(input, ctx)` verifier that returns `{ ok: true, sub?, claims? }` (custom claims are embedded in the token; reserved claims are stripped) or `{ ok: false, message }` (re-renders the login page; no code issued). Both are optional and default to the built-in email login. See `configure-auth.md` for a full example.
73
73
 
@@ -116,6 +116,50 @@ ENV FRONTMCP_BIND_ADDRESS=all
116
116
  EXPOSE 3000
117
117
  ```
118
118
 
119
+ ## DNS Rebinding Protection
120
+
121
+ **On by default since v1.7.2** (BC-035, GHSA-mc9g-v2cp-vfff). The server validates `Host`,
122
+ `X-Forwarded-Host` and `Origin` before routing and before reading the body; a request naming a host
123
+ this server does not answer to gets `403`. This covers the MCP endpoint, the OAuth routes, the SSE
124
+ transport and any custom route.
125
+
126
+ Binding loopback does **not** protect against this: a DNS rebinding attack points an
127
+ attacker-controlled domain at `127.0.0.1`, so loopback is the destination. CORS does not either —
128
+ after the rebind the browser genuinely considers the request same-origin. Validating `Host` is the
129
+ server-side defence.
130
+
131
+ With no `allowedHosts` configured, the list is derived from what the process listens on: `localhost`,
132
+ `127.0.0.1` and `[::1]`, each with and without the bound port, plus a specific bound NIC address.
133
+ Matching is case-insensitive and treats `host` and `host:80`/`host:443` as equal.
134
+
135
+ **Deployments behind a proxy need one line of config.** A routable bind (`0.0.0.0`, `::`, a specific
136
+ NIC) is reached under a hostname the process cannot know, so a derived list is not enforced there —
137
+ FrontMCP logs a warning and leaves host checking off rather than 403-ing a proxied deployment on a
138
+ patch upgrade. Name the public host to turn it on:
139
+
140
+ ```typescript
141
+ http: {
142
+ security: {
143
+ dnsRebindingProtection: {
144
+ allowedHosts: ['api.example.com', 'api.example.com:8443'],
145
+ allowedOrigins: ['https://app.example.com'],
146
+ },
147
+ },
148
+ }
149
+ ```
150
+
151
+ Or via the environment, which pairs with `FRONTMCP_BIND_ADDRESS` in a container:
152
+
153
+ ```dockerfile
154
+ ENV FRONTMCP_BIND_ADDRESS=all
155
+ ENV FRONTMCP_ALLOWED_HOSTS=api.example.com,api.example.com:8443
156
+ ```
157
+
158
+ To turn it off entirely: `dnsRebindingProtection: { enabled: false }`.
159
+
160
+ A request with **no** `Origin` header is allowed through — non-browser clients never send one, and a
161
+ rebound page always does. Rejecting the absent case breaks every CLI client and adds nothing.
162
+
119
163
  ## CORS Configuration
120
164
 
121
165
  ### No CORS Headers (Default)
@@ -45,7 +45,7 @@ Configure how clients connect to your FrontMCP server — SSE, Streamable HTTP,
45
45
  distributedMode: 'auto', // boolean | 'auto'
46
46
  eventStore: {
47
47
  enabled: true,
48
- provider: 'redis', // 'memory' | 'redis'
48
+ provider: 'redis', // 'memory' | 'redis' | 'sqlite'
49
49
  maxEvents: 10000,
50
50
  ttlMs: 300000,
51
51
  },
@@ -115,7 +115,7 @@ Enable event store so clients can resume SSE connections after disconnects:
115
115
  transport: {
116
116
  eventStore: {
117
117
  enabled: true,
118
- provider: 'redis', // 'memory' for single instance, 'redis' for distributed
118
+ provider: 'redis', // 'memory' | 'redis' | 'sqlite'
119
119
  maxEvents: 10000, // max events to store
120
120
  ttlMs: 300000, // 5 minute TTL
121
121
  redis: { provider: 'redis', host: 'localhost' },
@@ -123,6 +123,12 @@ transport: {
123
123
  }
124
124
  ```
125
125
 
126
+ **Auto-enabled in distributed mode.** A distributed deployment with Redis configured turns the event store on without an explicit `eventStore` block, so the notes below apply there too.
127
+
128
+ **Requires 1.7.2 or later.** Before 1.7.2 (GHSA-84j6-jc92-77jm) one store instance was shared by every session with no ownership check on replay, and the upstream transport writes every session's standalone SSE stream under the constant id `_GET_stream` with sequential event numbers — so a client sending `Last-Event-ID: _GET_stream:1` was replayed other sessions' server-to-client messages (tool results, resource contents, notifications). On 1.7.1 or earlier, do not enable the event store on a multi-tenant deployment.
129
+
130
+ From 1.7.2 each session gets a view over the shared store scoped to its own session id: an event id belonging to another session replays nothing.
131
+
126
132
  ## Target-Specific Recommendations
127
133
 
128
134
  | Target | Recommended Preset | Persistence | Event Store |
@@ -112,6 +112,8 @@ class DataServer {}
112
112
  ## What This Demonstrates
113
113
 
114
114
  - Declarative `permissions` as an array of `{ action, roles, scopes, custom }` rules
115
+ (**enforced from 1.7.2 onward** — see GHSA-58v2-gpcc-jmqv; earlier versions
116
+ stored the rules without evaluating them)
115
117
  - Using `tags` and `labels` for categorization and filtering
116
118
  - The `job()` function builder for simple jobs that need no class
117
119
  - Full server registration with `jobs.enabled: true` and a Redis store
@@ -35,17 +35,17 @@ Create a class extending `JobContext<In, Out>` and implement the `execute(input:
35
35
 
36
36
  ### JobMetadata Fields
37
37
 
38
- | Field | Type | Required | Default | Description |
39
- | -------------- | ------------------------ | -------- | ---------------- | ------------------------------------------ |
40
- | `name` | `string` | Yes | -- | Unique job name |
41
- | `inputSchema` | `ZodRawShape` | Yes | -- | Zod raw shape for input validation |
42
- | `outputSchema` | `ZodRawShape \| ZodType` | Yes | -- | Zod schema for output validation |
43
- | `description` | `string` | No | -- | Human-readable description |
44
- | `timeout` | `number` | No | `300000` (5 min) | Maximum execution time in milliseconds |
45
- | `retry` | `RetryPolicy` | No | -- | Retry configuration (see below) |
46
- | `tags` | `string[]` | No | -- | Categorization tags |
47
- | `labels` | `Record<string, string>` | No | -- | Key-value labels for filtering |
48
- | `permissions` | `JobPermission[]` | No | -- | Array of permission rules (one per action) |
38
+ | Field | Type | Required | Default | Description |
39
+ | -------------- | ------------------------ | -------- | ---------------- | -------------------------------------------------------------------------------------------------- |
40
+ | `name` | `string` | Yes | -- | Unique job name |
41
+ | `inputSchema` | `ZodRawShape` | Yes | -- | Zod raw shape for input validation |
42
+ | `outputSchema` | `ZodRawShape \| ZodType` | Yes | -- | Zod schema for output validation |
43
+ | `description` | `string` | No | -- | Human-readable description |
44
+ | `timeout` | `number` | No | `300000` (5 min) | Maximum execution time in milliseconds |
45
+ | `retry` | `RetryPolicy` | No | -- | Retry configuration (see below) |
46
+ | `tags` | `string[]` | No | -- | Categorization tags |
47
+ | `labels` | `Record<string, string>` | No | -- | Key-value labels for filtering |
48
+ | `permissions` | `JobPermission[]` | No | -- | Array of permission rules. Multiple rules may target the same action; all matching rules must pass |
49
49
 
50
50
  ### Basic Example
51
51
 
@@ -266,13 +266,17 @@ class ImportCsvJob extends JobContext {
266
266
 
267
267
  Control who can interact with jobs using the `permissions` field. **`permissions` is an array** of rules; each rule grants access to a single `action` and lists the roles, scopes, and/or custom predicate required for that action.
268
268
 
269
+ **Requires 1.7.2 or later.** Before 1.7.2 (GHSA-58v2-gpcc-jmqv) the `permissions` array was validated and stored but never evaluated — every job was reachable by every caller who could reach `execute_job`. On older versions do not rely on this field for access control.
270
+
271
+ Semantics: no rules for an action means allow (the documented default); once any rule targets an action, **all** rules for that action must pass, and `roles`/`scopes` within a single rule are **any-of**. Enforcement happens in `JobExecutionManager`, so background runs and workflow steps are covered, and `list_jobs` hides entries the caller could not run. A denial is indistinguishable from "not found" so restricted job names cannot be enumerated.
272
+
269
273
  ### Permission Rule Shape
270
274
 
271
275
  ```typescript
272
276
  interface JobPermission {
273
277
  action: 'create' | 'read' | 'update' | 'delete' | 'execute' | 'list'; // singular!
274
- roles?: string[]; // user must have one of these roles
275
- scopes?: string[]; // token must include all of these scopes
278
+ roles?: string[]; // caller must have one of these roles
279
+ scopes?: string[]; // token must include one of these scopes
276
280
  custom?: (authInfo: Partial<Record<string, unknown>>) => boolean | Promise<boolean>;
277
281
  }
278
282
  ```
@@ -418,7 +422,7 @@ class DataApp {}
418
422
 
419
423
  ### Enabling the Jobs System
420
424
 
421
- **Auto-enable (issue #408):** declaring any `@App({ jobs: [...] })` (or `workflows: [...]`) is enough — the jobs subsystem comes up with in-memory stores by default and the management tools (`execute_job`, `list_jobs`, `get_job_status`, `register_job`, `remove_job`) are registered automatically so agents can invoke them. No `@FrontMcp({ jobs: { enabled: true } })` is required for the happy path.
425
+ **Auto-enable (issue #408):** declaring any `@App({ jobs: [...] })` (or `workflows: [...]`) is enough — the jobs subsystem comes up with in-memory stores by default and the management tools (`execute_job`, `list_jobs`, `get_job_status`, `remove_job`) are registered automatically so agents can invoke them. `register_job` is added only when `jobs.allowDynamicRegistration` is `true`. No `@FrontMcp({ jobs: { enabled: true } })` is required for the happy path.
422
426
 
423
427
  **When to configure `@FrontMcp({ jobs })` explicitly:** override the in-memory default with persistent storage (Redis recommended for multi-replica HA) so job state, progress, logs, and outputs survive retries and server restarts.
424
428
 
@@ -450,17 +454,27 @@ Setting `jobs: { enabled: false }` is an explicit opt-out — declared jobs will
450
454
 
451
455
  Once jobs are registered, the SDK exposes five MCP tools (snake_case per ecosystem convention; hyphen aliases like `execute-job` keep working with a deprecation log line for one release):
452
456
 
453
- | Tool | Purpose |
454
- | ---------------- | ------------------------------------------------------------------------ |
455
- | `list_jobs` | List registered jobs with optional `tags` / `labels` / `query` filters |
456
- | `execute_job` | Execute a registered job by name (`{ name, input?, background? }`) |
457
- | `get_job_status` | Get the run state for a `runId` returned by `execute_job` |
458
- | `register_job` | Register a dynamic job at runtime (sandboxed; `hideFromDiscovery: true`) |
459
- | `remove_job` | Remove a dynamic job by name (`hideFromDiscovery: true`) |
457
+ | Tool | Purpose |
458
+ | ---------------- | ---------------------------------------------------------------------- |
459
+ | `list_jobs` | List registered jobs with optional `tags` / `labels` / `query` filters |
460
+ | `execute_job` | Execute a registered job by name (`{ name, input?, background? }`) |
461
+ | `get_job_status` | Get the run state for a `runId` returned by `execute_job` |
462
+ | `register_job` | Register a dynamic job at runtime — **opt-in**, see below |
463
+ | `remove_job` | Remove a dynamic job by name (`hideFromDiscovery: true`) |
460
464
 
461
465
  Workflows expose a parallel set: `list_workflows`, `execute_workflow`, `get_workflow_status`, `register_workflow`, `remove_workflow`.
462
466
 
463
- For finer-grained control (e.g. omit `register_job` / `remove_job` in production), opt out of auto-registration by importing the tool classes manually:
467
+ `register_job` / `register_workflow` take a **raw script string** and register it as an executable job. Since 1.7.2 they are not registered unless you opt in:
468
+
469
+ ```typescript
470
+ @FrontMcp({ jobs: { enabled: true, allowDynamicRegistration: true } })
471
+ ```
472
+
473
+ Leave it off unless an agent is genuinely meant to author jobs — with it on, any caller who reaches the tool list can author and run code on the server.
474
+
475
+ `get_job_status` / `get_workflow_status` return only runs started by the calling subject; a run record carries the job's inputs and results, so a foreign `runId` reads as "not found".
476
+
477
+ For finer-grained control, opt out of auto-registration by importing the tool classes manually:
464
478
 
465
479
  ```typescript
466
480
  import { App, ExecuteJobTool, GetJobStatusTool, ListJobsTool } from '@frontmcp/sdk';
@@ -35,16 +35,16 @@ Create a class decorated with `@Workflow`. The decorator requires `name` and `st
35
35
 
36
36
  ### WorkflowMetadata Fields
37
37
 
38
- | Field | Type | Required | Default | Description |
39
- | ---------------- | ---------------------------------- | ----------- | ----------------- | ----------------------------------------------------- |
40
- | `name` | `string` | Yes | -- | Unique workflow name |
41
- | `steps` | `WorkflowStep[]` | Yes (min 1) | -- | Array of step definitions |
42
- | `description` | `string` | No | -- | Human-readable description |
43
- | `trigger` | `'manual' \| 'webhook' \| 'event'` | No | `'manual'` | How the workflow is initiated |
44
- | `webhook` | `WebhookConfig` | No | -- | Webhook configuration (when trigger is `'webhook'`) |
45
- | `timeout` | `number` | No | `600000` (10 min) | Maximum total workflow execution time in milliseconds |
46
- | `maxConcurrency` | `number` | No | `5` | Maximum number of steps running in parallel |
47
- | `permissions` | `WorkflowPermission[]` | No | -- | Array of permission rules (one per action) |
38
+ | Field | Type | Required | Default | Description |
39
+ | ---------------- | ---------------------------------- | ----------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
40
+ | `name` | `string` | Yes | -- | Unique workflow name |
41
+ | `steps` | `WorkflowStep[]` | Yes (min 1) | -- | Array of step definitions |
42
+ | `description` | `string` | No | -- | Human-readable description |
43
+ | `trigger` | `'manual' \| 'webhook' \| 'event'` | No | `'manual'` | How the workflow is initiated |
44
+ | `webhook` | `WebhookConfig` | No | -- | Webhook configuration (when trigger is `'webhook'`) |
45
+ | `timeout` | `number` | No | `600000` (10 min) | Maximum total workflow execution time in milliseconds |
46
+ | `maxConcurrency` | `number` | No | `5` | Maximum number of steps running in parallel |
47
+ | `permissions` | `JobPermission[]` | No | -- | Array of permission rules. Multiple rules may target the same action; all matching rules must pass. Same shape and semantics as job permissions; **enforced from 1.7.2** (GHSA-58v2-gpcc-jmqv) |
48
48
 
49
49
  ### WorkflowStep Fields
50
50
 
@@ -662,9 +662,23 @@ interface DashboardPluginOptionsInput {
662
662
 
663
663
  - `enabled` -- When omitted, the dashboard is automatically enabled in development (`NODE_ENV !== 'production'`) and disabled in production.
664
664
  - `basePath` -- URL path where the dashboard is served. Default: `'/dashboard'`.
665
- - `auth.token` -- When set, the dashboard requires `?token=<value>` as a query parameter.
665
+ - `auth.enabled` / `auth.token` -- Gate the dashboard page on a shared secret. Present it as `Authorization: Bearer <token>` (preferred) or `?token=<value>`. `enabled: true` without a `token` is a **startup error** — the server refuses to boot rather than serve an "authenticated" dashboard with nothing to check. The token is compared in constant time and is never embedded in the served page.
666
666
  - `cdn` -- Override default CDN URLs for the dashboard UI bundle and its dependencies. Useful for air-gapped environments.
667
667
 
668
+ ### Security
669
+
670
+ **Requires 1.7.2 or later.** Before 1.7.2 (GHSA-rgxj-434m-vxh3) `auth.token` was documented and validated but never checked, the operator's options never reached the middleware at all, and the dashboard's MCP scope declared `auth: { mode: 'public' }` unconditionally — so a dashboard configured with a secret still served its page, and `dashboard:graph` still returned the entire server's inventory, to anyone. On 1.7.1 or earlier, treat any server running `DashboardApp` as publicly introspectable.
671
+
672
+ The dashboard's MCP scope **inherits the server's authentication**. Its introspection tools (`dashboard:graph`, `dashboard:list-tools`, `dashboard:list-resources`) reach the root scope and enumerate every app, tool, resource and prompt on the server — including names, descriptions and (on request) schemas. Two consequences:
673
+
674
+ - On an authenticated server (`local`, `remote`, `transparent`, `orchestrated`), the dashboard requires the same credential as everything else.
675
+ - On a **public** server the dashboard is public too, because the server is. `auth.token` gates the dashboard _page_, not the MCP scope or the SSE stream. If the inventory is sensitive, authenticate the server — do not rely on the dashboard token alone.
676
+
677
+ Two further limitations worth knowing:
678
+
679
+ - The token is accepted as `Authorization: Bearer <token>` or `?token=`. Prefer the header: a URL token lands in browser history, `Referer` headers and access logs. There is no cookie/session option yet.
680
+ - Dashboard options are **process-wide**. Two `@FrontMcp` servers built in one process that configure the dashboard differently share the last configuration registered, including its token; the plugin logs a warning when that happens. Run one dashboard per process.
681
+
668
682
  ---
669
683
 
670
684
  ## Registration Pattern
@@ -29,6 +29,15 @@ These checks apply to ALL deployment targets. Run them first, then proceed to yo
29
29
  - [ ] Credentials mode is only enabled if cookies/sessions are needed
30
30
  - [ ] Preflight cache (`maxAge`) is set to reduce OPTIONS requests
31
31
 
32
+ ### DNS Rebinding Protection
33
+
34
+ - [ ] `security.dnsRebindingProtection.allowedHosts` (or `FRONTMCP_ALLOWED_HOSTS`) names the public
35
+ hostname(s) — on a routable bind the derived default is **not** enforced, and FrontMCP logs a
36
+ warning saying so
37
+ - [ ] Startup logs show no `DNS-rebinding protection is not enforcing a Host allow-list` warning
38
+ - [ ] Include the port when the public URL uses a non-default one (`api.example.com:8443`)
39
+ - [ ] `allowedOrigins` is set when a browser client connects, so a foreign `Origin` is refused
40
+
32
41
  ### Input Validation
33
42
 
34
43
  - [ ] All tool inputs use Zod schemas (never trust raw input)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/skills",
3
- "version": "1.7.1",
3
+ "version": "1.7.2",
4
4
  "description": "Curated skills catalog for FrontMCP projects",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://docs.agentfront.dev",