@happyvertical/smrt-app-mcp 0.53.3 → 0.53.5
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/AGENTS.md +23 -0
- package/README.md +154 -0
- package/dist/auth.d.ts +114 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +147 -9
- package/dist/auth.js.map +1 -1
- package/dist/chunks/protocol-BlbEg3jn.js +7647 -0
- package/dist/chunks/protocol-BlbEg3jn.js.map +1 -0
- package/dist/defaults.d.ts +87 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -7351
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/own-data.d.ts +21 -0
- package/dist/own-data.d.ts.map +1 -0
- package/dist/server.d.ts +41 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/dist/sveltekit.d.ts +161 -1
- package/dist/sveltekit.d.ts.map +1 -1
- package/dist/sveltekit.js +158 -3
- package/dist/sveltekit.js.map +1 -1
- package/dist/tools.d.ts +18 -0
- package/dist/tools.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/chunks/protocol-Ce0U_XSp.js +0 -184
- package/dist/chunks/protocol-Ce0U_XSp.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -22,6 +22,29 @@ tool policy; `./sveltekit` owns the stateless Streamable HTTP transport.
|
|
|
22
22
|
validates JWT access tokens and requires an application-owned principal mapper.
|
|
23
23
|
- Mount `mountMcpRoute` as one modern `POST` endpoint. It creates a fresh
|
|
24
24
|
protocol server per request, emits no session id, and refuses subscriptions.
|
|
25
|
+
- `mountMcpAppRoute` (`src/defaults.ts` + `./sveltekit`) is the one-call app
|
|
26
|
+
route: explicit `models` allow-list, required `requiredScopes`, principal from
|
|
27
|
+
session locals (`principalFromSessionLocals`). App policies compose with the
|
|
28
|
+
default and may only narrow it. Its `Origin` check (default on; opt-in on
|
|
29
|
+
`mountMcpRoute`) runs before auth and dispatch; no-`Origin` clients pass.
|
|
30
|
+
`effects` filters the catalog by `mcpToolEffect` (excluded tools 404).
|
|
31
|
+
`auth` swaps in bearer principals; `bindPrincipal` runs their dispatch in
|
|
32
|
+
the app's principal context (RLS transaction), failing closed, and
|
|
33
|
+
authorizes with the binder's narrowed (live) scopes; task lifecycle calls
|
|
34
|
+
re-check the originating action against the allow-list and `effects`,
|
|
35
|
+
after a principal-level `taskPrincipalPolicy` (defaults: scope policy);
|
|
36
|
+
`mountMcpProtectedResourceMetadataRoute` serves only the advertised path.
|
|
37
|
+
- `createHostedMcpResourceAuth({ profile, runtime })`: locally, a runtime
|
|
38
|
+
`verifyLocalMcpToken` yields the `sessionFallback` adapter
|
|
39
|
+
(`createLocalMcpTokenAuth`): no `Authorization` header keeps the session
|
|
40
|
+
principal; a presented bearer must verify (401, never cookie fallback); no
|
|
41
|
+
metadata (404). Hosted, `runtime.resolveMcpPrincipal` is the default
|
|
42
|
+
identity mapping; an explicit `resolvePrincipal` overrides it.
|
|
43
|
+
- Adapters stamp `principal.tenantBinding` for `bindPrincipal`: the local
|
|
44
|
+
token adapter always `direct`; the JWT adapter the mapping's value or
|
|
45
|
+
`direct-or-inherited`. Never derive it from request input. Both adapters
|
|
46
|
+
read principals/mappings from own data only (`own-data.ts`): inherited
|
|
47
|
+
values, accessors and poison keys deny.
|
|
25
48
|
- Keep tool catalogs private by default. Public caching requires an explicit
|
|
26
49
|
attestation and a global, unauthenticated, read-only, non-tenant catalog with
|
|
27
50
|
no principal-aware policy.
|
package/README.md
CHANGED
|
@@ -8,6 +8,160 @@ App-runtime MCP server scaffolding for s-m-r-t apps. Provides:
|
|
|
8
8
|
`mountMcpToolsRoute` / `mountMcpCallRoute` aliases remain available for one
|
|
9
9
|
release while applications migrate.
|
|
10
10
|
|
|
11
|
+
## One-call app route
|
|
12
|
+
|
|
13
|
+
Most applications need no server helper module. `mountMcpAppRoute` builds the
|
|
14
|
+
server from the app's own declared models, maps the request principal from the
|
|
15
|
+
SvelteKit session locals populated by `createSessionHandler`
|
|
16
|
+
(`@happyvertical/smrt-users/sveltekit`), and mounts the stateless endpoint:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// src/routes/api/mcp/+server.ts
|
|
20
|
+
import { mountMcpAppRoute } from '@happyvertical/smrt-app-mcp/sveltekit';
|
|
21
|
+
import { Item } from '$lib/objects/Item';
|
|
22
|
+
// The app's `createSmrtSvelteKitRuntime()` (@happyvertical/smrt-app-runtime/sveltekit).
|
|
23
|
+
import { runtime } from '$lib/server/smrt';
|
|
24
|
+
|
|
25
|
+
export const POST = mountMcpAppRoute({
|
|
26
|
+
models: [Item],
|
|
27
|
+
requiredScopes: ['items.read'],
|
|
28
|
+
// Per request: under RLS this is the request's transaction-bound database.
|
|
29
|
+
smrtOptions: () => ({ db: runtime.databaseConfig() }),
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- **Allow-list** — exactly the `models` listed (registered `@smrt()`
|
|
34
|
+
constructors; anything else throws at construction). Other registered
|
|
35
|
+
models are never enumerated; direct calls to them return the unknown-tool
|
|
36
|
+
error (404 semantics).
|
|
37
|
+
- **Principal** — `principalFromSessionLocals` reads only `locals.user.id`, the
|
|
38
|
+
session-authorized `locals.tenantId`, and `locals.permissions` (as sorted
|
|
39
|
+
`scopes`, `kind: 'human'`). Missing or malformed fields mean unauthenticated.
|
|
40
|
+
URL-selected tenants, headers and bodies are never identity inputs. Override
|
|
41
|
+
with `resolvePrincipal`.
|
|
42
|
+
- **Policy** — `requiredScopes` is required: every authenticated principal must
|
|
43
|
+
be an accepted kind (`principalKinds`, default `['human']`) with an id, a
|
|
44
|
+
tenant, and every listed scope, for every tool and resource. Unauthenticated
|
|
45
|
+
callers keep the base rule (only `publicToolPatterns` read-only tools; none by
|
|
46
|
+
default) and mutating tools always need a principal. A supplied `toolPolicy`
|
|
47
|
+
or `resourcePolicy` is composed with this default and can only narrow it;
|
|
48
|
+
for a wider policy, call `createMcpAppServer` and `mountMcpRoute` directly.
|
|
49
|
+
Because the default is principal-aware, `tools/list` stays `private` even
|
|
50
|
+
with a public-cache attestation. A scope applies uniformly to every
|
|
51
|
+
published tool, so a read scope such as `items.read` must be paired with
|
|
52
|
+
`effects: ['read']` (below) unless the app adds per-operation policy.
|
|
53
|
+
- **Effects** — `effects` (also on `createMcpAppServer`) limits the catalog to
|
|
54
|
+
tools whose effect is listed: `'read'`, `'write'`, `'destructive'`, the
|
|
55
|
+
WebMCP vocabulary. `mcpToolEffect` classifies with the existing read-only
|
|
56
|
+
detection (canonical `readOnlyHint`, else the `_list`/`_get` name rule); any
|
|
57
|
+
other tool is `destructive` unless it declares `destructiveHint: false`.
|
|
58
|
+
Excluded tools are absent from `tools/list`, and a direct call gets the same
|
|
59
|
+
unknown-tool error as a nonexistent name, so they cannot be enumerated.
|
|
60
|
+
Omitted, every allow-listed tool is published (unchanged behaviour).
|
|
61
|
+
Durable task lifecycle calls (`tasks/get`, `tasks/update`, `tasks/cancel`)
|
|
62
|
+
re-apply the allow-list and `effects` filter to the task's originating
|
|
63
|
+
action, the generated tool for the class (simple or qualified name) and
|
|
64
|
+
method its job persists; a task whose action is excluded, or maps to no
|
|
65
|
+
allow-listed generated tool, is answered exactly like an unknown task.
|
|
66
|
+
`toolPolicy` is not re-applied to lifecycle calls: a task created by an
|
|
67
|
+
application workflow does not record which workflow created it, and such
|
|
68
|
+
workflows often front a policy-hidden generated task tool. Owner and tenant
|
|
69
|
+
scoping apply to every lifecycle call. Principal-level authority is checked
|
|
70
|
+
first, before the task store is touched: `createDefaultMcpAppServer` (and so
|
|
71
|
+
`mountMcpAppRoute`) gates every lifecycle call on its principal scope policy
|
|
72
|
+
— accepted kind, id, tenant and every `requiredScopes` entry, evaluated on
|
|
73
|
+
the *effective* bound principal — so a bearer whose live permissions were
|
|
74
|
+
revoked cannot read, resume (supply input to) or cancel an existing task;
|
|
75
|
+
restoring the permission restores access. A custom `createMcpAppServer`
|
|
76
|
+
supplies the same gate with `taskPrincipalPolicy({ principal })`; without
|
|
77
|
+
it, a custom `toolPolicy` is not applied to lifecycle calls. A denial or a
|
|
78
|
+
thrown predicate is answered as an unknown task.
|
|
79
|
+
- **Server** — `serverInfo` defaults to `{ name: 'smrt-app', version: '0.1.0' }`;
|
|
80
|
+
every other `createMcpAppServer` option (`workflowTools`, `resources`,
|
|
81
|
+
`workflowAssertions`, `toolListCache`, …) passes through. The handler's
|
|
82
|
+
`server` property exposes the policy core. The same defaults are available
|
|
83
|
+
without SvelteKit as `createDefaultMcpAppServer` from the root entry.
|
|
84
|
+
|
|
85
|
+
- **Origin check** — on by default (`checkOrigin`). Before bearer
|
|
86
|
+
authentication, principal resolution or any dispatch, a request whose
|
|
87
|
+
`Origin` is present but is not the request URL's own origin (exact scheme,
|
|
88
|
+
host and port) or one of `trustedOrigins` — including `Origin: null` and
|
|
89
|
+
sibling subdomains — gets HTTP 403 with the JSON-RPC error data
|
|
90
|
+
`{ code: 'mcp_origin_denied', retryable: false }`, naming no origins. With
|
|
91
|
+
no `Origin`, a `Sec-Fetch-Site` of `cross-site`/`same-site` is also
|
|
92
|
+
refused. Clients that send no browser origin signals (server-side MCP
|
|
93
|
+
clients, the `smrt-app-cli` bridge) are unaffected; a browser-hosted MCP
|
|
94
|
+
client must be listed in `trustedOrigins`. Bearer requests are checked too:
|
|
95
|
+
the token is not ambient authority, but the MCP Streamable HTTP transport
|
|
96
|
+
requires origin validation and server-side remote clients send no `Origin`.
|
|
97
|
+
"Own origin" is `event.url`, so behind a rewriting proxy configure the
|
|
98
|
+
SvelteKit adapter's origin (adapter-node `ORIGIN`, or its
|
|
99
|
+
`PROTOCOL_HEADER`/`HOST_HEADER`) or add the public origin to
|
|
100
|
+
`trustedOrigins`; this package never reads `X-Forwarded-*`.
|
|
101
|
+
`checkOrigin: false` opts out. `mountMcpRoute` accepts the same options,
|
|
102
|
+
off unless set.
|
|
103
|
+
|
|
104
|
+
Bearer authentication for hosted profiles is an `auth` option on both
|
|
105
|
+
`mountMcpRoute` and `mountMcpAppRoute`. When its source yields an adapter,
|
|
106
|
+
every request must carry a valid bearer token and the adapter's principal
|
|
107
|
+
replaces the session principal; `null` (the `local` profile) keeps the session
|
|
108
|
+
principal. Pass `bindPrincipal` (for example `runtime.runAsPrincipal` from
|
|
109
|
+
`@happyvertical/smrt-app-runtime/sveltekit`) so that, after bearer
|
|
110
|
+
authentication, the `smrtOptions` database thunk, task handling and tool
|
|
111
|
+
execution all run inside a context bound to that principal: under
|
|
112
|
+
`database-rls` the runtime opens a fresh RLS transaction publishing the bearer
|
|
113
|
+
user, tenant and its live membership permissions capped by the token's scopes,
|
|
114
|
+
instead of the anonymous (or a cookie user's) request transaction. The binder
|
|
115
|
+
hands dispatch the principal's effective scopes (`runAsPrincipal` passes the
|
|
116
|
+
token scopes still granted by live permissions), and the tool policy
|
|
117
|
+
authorizes with those, intersected with the token's: a revoked permission
|
|
118
|
+
denies the tool before dispatch even under `application` isolation, while the
|
|
119
|
+
principal keeps the tools its remaining permissions allow. A binder can never
|
|
120
|
+
widen scopes or change the principal's id or tenant. A binder
|
|
121
|
+
that fails before dispatch answers HTTP 403 with the safe
|
|
122
|
+
`mcp_tool_access_denied` error; the response is materialized inside the
|
|
123
|
+
binding. `createHostedMcpResourceAuth` from `./auth` builds that source from
|
|
124
|
+
`SMRT_MCP_RESOURCE`, `SMRT_MCP_ISSUER`, `SMRT_MCP_JWKS_URI` and
|
|
125
|
+
`SMRT_MCP_SCOPES`, caching one adapter and retrying a failed construction:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { createHostedMcpResourceAuth } from '@happyvertical/smrt-app-mcp/auth';
|
|
129
|
+
const { profile } = await runtime.resolvedRuntime();
|
|
130
|
+
// `runtime` supplies both credential bindings (see below); pass
|
|
131
|
+
// `resolvePrincipal` as well to override the hosted identity mapping.
|
|
132
|
+
const auth = createHostedMcpResourceAuth({ profile, runtime });
|
|
133
|
+
// api/mcp/+server.ts
|
|
134
|
+
export const POST = mountMcpAppRoute({
|
|
135
|
+
models: [Item], requiredScopes: ['items.read'], effects: ['read'],
|
|
136
|
+
smrtOptions, auth, bindPrincipal: runtime.runAsPrincipal,
|
|
137
|
+
});
|
|
138
|
+
// .well-known/oauth-protected-resource/api/mcp/+server.ts
|
|
139
|
+
export const GET = mountMcpProtectedResourceMetadataRoute(auth);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The `runtime` option takes any object with the optional
|
|
143
|
+
`verifyLocalMcpToken(token)` and `resolveMcpPrincipal(identity)` methods; the
|
|
144
|
+
s-m-r-t SvelteKit runtime has both.
|
|
145
|
+
|
|
146
|
+
- **Local profile.** With `verifyLocalMcpToken`, the source yields the
|
|
147
|
+
`createLocalMcpTokenAuth` adapter for owner-minted tokens
|
|
148
|
+
(`smrt app token --scopes items.read`). A request with no `Authorization`
|
|
149
|
+
header keeps the session principal, exactly as before. A presented bearer
|
|
150
|
+
must verify: an unknown, revoked or expired token is a bare 401
|
|
151
|
+
(`WWW-Authenticate: Bearer error="invalid_token"`) and never falls back to
|
|
152
|
+
the cookie. The origin check still runs first. Without the runtime the
|
|
153
|
+
source yields `null`, as before.
|
|
154
|
+
- **Hosted profiles.** Without `resolvePrincipal`, the runtime's
|
|
155
|
+
membership-backed `resolveMcpPrincipal` maps the verified issuer/subject to
|
|
156
|
+
a user linked through `oidc_identities` with exactly one active tenant
|
|
157
|
+
membership, and denies anything else. Pass `resolvePrincipal` when users
|
|
158
|
+
belong to several tenants.
|
|
159
|
+
|
|
160
|
+
`mountMcpProtectedResourceMetadataRoute` serves the RFC 9728 document only at
|
|
161
|
+
the adapter's advertised `metadataUrl` path. It returns 404 for any other
|
|
162
|
+
path, and in the `local` profile (no adapter, or the local token adapter). It
|
|
163
|
+
does not implement an OAuth authorization server.
|
|
164
|
+
|
|
11
165
|
For piping a deployed app's MCP surface to a local stdio MCP client, see `@happyvertical/smrt-app-cli` — the client-side runtime CLI exposes a `startMcpBridge()` default and a generic `smrt-mcp-bridge` bin.
|
|
12
166
|
|
|
13
167
|
For public deployments, follow the
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Server-only OAuth protected-resource boundary. This is not an issuer. */
|
|
2
2
|
import { type JWTPayload } from 'jose';
|
|
3
|
-
import type { McpAppPrincipal } from './server.js';
|
|
3
|
+
import type { McpAppPrincipal, McpTenantBinding } from './server.js';
|
|
4
4
|
export type McpDeploymentProfile = 'local' | 'self-hosted' | 'cloud';
|
|
5
5
|
export interface McpVerifiedIdentity {
|
|
6
6
|
readonly issuer: string;
|
|
@@ -13,6 +13,12 @@ export interface McpPrincipalMapping {
|
|
|
13
13
|
tenantId?: string;
|
|
14
14
|
kind?: string;
|
|
15
15
|
roles?: string[];
|
|
16
|
+
/**
|
|
17
|
+
* How the mapped tenant authority may be bound. Defaults to
|
|
18
|
+
* `direct-or-inherited`; a mapping that selected a direct membership
|
|
19
|
+
* returns `direct` so inheritance can never substitute for it.
|
|
20
|
+
*/
|
|
21
|
+
tenantBinding?: McpTenantBinding;
|
|
16
22
|
}
|
|
17
23
|
export interface McpResourceAuthOptions {
|
|
18
24
|
profile: McpDeploymentProfile;
|
|
@@ -35,9 +41,21 @@ export interface McpResourceAuthOptions {
|
|
|
35
41
|
resolvePrincipal(identity: McpVerifiedIdentity): Promise<McpPrincipalMapping | null>;
|
|
36
42
|
}
|
|
37
43
|
export interface McpResourceAuth {
|
|
38
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Mount GET at this URL. Paths use RFC 9728's well-known path insertion.
|
|
46
|
+
* Empty for a {@link McpResourceAuth.sessionFallback} adapter, which
|
|
47
|
+
* advertises no OAuth metadata.
|
|
48
|
+
*/
|
|
39
49
|
readonly metadataUrl: string;
|
|
40
50
|
metadataResponse(): Response;
|
|
51
|
+
/**
|
|
52
|
+
* When `true`, a request that carries no `Authorization` header is not
|
|
53
|
+
* challenged: the route resolves its session principal instead (today's
|
|
54
|
+
* local-profile behaviour). A presented bearer must still verify; an
|
|
55
|
+
* invalid one is refused and never falls back to the session. Only the
|
|
56
|
+
* local owner-token adapter ({@link createLocalMcpTokenAuth}) sets it.
|
|
57
|
+
*/
|
|
58
|
+
readonly sessionFallback?: boolean;
|
|
41
59
|
/** A failure response contains no token, claims or upstream error details. */
|
|
42
60
|
authenticate(request: Request): Promise<{
|
|
43
61
|
ok: true;
|
|
@@ -49,5 +67,99 @@ export interface McpResourceAuth {
|
|
|
49
67
|
response: Response;
|
|
50
68
|
}>;
|
|
51
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Credential bindings an application runtime can supply to
|
|
72
|
+
* {@link createHostedMcpResourceAuth}, structurally satisfied by the SMRT
|
|
73
|
+
* SvelteKit runtime from `@happyvertical/smrt-app-runtime/sveltekit`.
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* A verified local-token principal as a runtime returns it. Its `scopes` may
|
|
77
|
+
* be a frozen (readonly) array; the adapter copies it.
|
|
78
|
+
*/
|
|
79
|
+
export type McpLocalTokenPrincipal = Omit<McpAppPrincipal, 'scopes'> & {
|
|
80
|
+
id: string;
|
|
81
|
+
scopes?: readonly string[];
|
|
82
|
+
};
|
|
83
|
+
export interface McpAuthRuntime {
|
|
84
|
+
/**
|
|
85
|
+
* Verify an owner-minted local MCP token (`smrt app token`). Resolve the
|
|
86
|
+
* token principal (with scopes already capped to live permissions), or
|
|
87
|
+
* `null` to deny. Used only in the `local` profile.
|
|
88
|
+
*/
|
|
89
|
+
verifyLocalMcpToken?(token: string): Promise<McpLocalTokenPrincipal | null>;
|
|
90
|
+
/**
|
|
91
|
+
* Default hosted identity mapping, used when no explicit
|
|
92
|
+
* `resolvePrincipal` is given (for example the runtime's membership-backed
|
|
93
|
+
* resolver).
|
|
94
|
+
*/
|
|
95
|
+
resolveMcpPrincipal?(identity: McpVerifiedIdentity): Promise<McpPrincipalMapping | null>;
|
|
96
|
+
}
|
|
97
|
+
/** Options for {@link createLocalMcpTokenAuth}. */
|
|
98
|
+
export interface LocalMcpTokenAuthOptions {
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a presented token to its principal, or `null` to deny. A thrown
|
|
101
|
+
* error is also a denial. The principal needs a non-empty `id` and
|
|
102
|
+
* `tenantId` and string `scopes`; `allowCrossTenant` is never honoured.
|
|
103
|
+
*/
|
|
104
|
+
verify(token: string): Promise<McpLocalTokenPrincipal | null>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Bearer adapter for owner-minted local MCP tokens. It is a
|
|
108
|
+
* {@link McpResourceAuth.sessionFallback} adapter: without an `Authorization`
|
|
109
|
+
* header the route keeps its session principal; with one, the token must
|
|
110
|
+
* verify, otherwise the response is `401` with
|
|
111
|
+
* `WWW-Authenticate: Bearer error="invalid_token"` and no token, claim or
|
|
112
|
+
* upstream detail. It advertises no OAuth metadata (the metadata route 404s).
|
|
113
|
+
*/
|
|
114
|
+
export declare function createLocalMcpTokenAuth(options: LocalMcpTokenAuthOptions): McpResourceAuth;
|
|
115
|
+
/** Options for {@link createHostedMcpResourceAuth}. */
|
|
116
|
+
export interface HostedMcpResourceAuthOptions {
|
|
117
|
+
/**
|
|
118
|
+
* Deployment profile, or a thunk read on every call. `local` disables bearer
|
|
119
|
+
* authentication; the route then uses its session principal.
|
|
120
|
+
*/
|
|
121
|
+
profile: McpDeploymentProfile | (() => McpDeploymentProfile);
|
|
122
|
+
/**
|
|
123
|
+
* Application-owned mapping from a verified identity to a current account
|
|
124
|
+
* and active membership; token claims never select a tenant. Overrides
|
|
125
|
+
* `runtime.resolveMcpPrincipal`. A hosted profile with neither throws on
|
|
126
|
+
* first use.
|
|
127
|
+
*/
|
|
128
|
+
resolvePrincipal?: McpResourceAuthOptions['resolvePrincipal'];
|
|
129
|
+
/**
|
|
130
|
+
* The application runtime (for example the SMRT SvelteKit runtime). In the
|
|
131
|
+
* `local` profile its `verifyLocalMcpToken` turns on owner-minted bearer
|
|
132
|
+
* tokens through {@link createLocalMcpTokenAuth} (requests without a
|
|
133
|
+
* bearer keep the session principal); without it the thunk yields `null`
|
|
134
|
+
* as before. In hosted profiles its membership-backed
|
|
135
|
+
* `resolveMcpPrincipal` is the default identity mapping.
|
|
136
|
+
*/
|
|
137
|
+
runtime?: McpAuthRuntime;
|
|
138
|
+
/** Environment source. Defaults to `process.env`, read lazily. */
|
|
139
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
140
|
+
/** Default `['RS256']`. */
|
|
141
|
+
algorithms?: McpResourceAuthOptions['algorithms'];
|
|
142
|
+
tokenType?: McpResourceAuthOptions['tokenType'];
|
|
143
|
+
requireTenant?: McpResourceAuthOptions['requireTenant'];
|
|
144
|
+
}
|
|
145
|
+
/** Environment variables read by {@link createHostedMcpResourceAuth}. */
|
|
146
|
+
export declare const HOSTED_MCP_AUTH_ENV: Readonly<{
|
|
147
|
+
resource: "SMRT_MCP_RESOURCE";
|
|
148
|
+
issuer: "SMRT_MCP_ISSUER";
|
|
149
|
+
jwksUri: "SMRT_MCP_JWKS_URI";
|
|
150
|
+
scopes: "SMRT_MCP_SCOPES";
|
|
151
|
+
}>;
|
|
152
|
+
/**
|
|
153
|
+
* Lazily build the hosted protected-resource adapter from operator
|
|
154
|
+
* environment. The returned thunk yields `null` for the `local` profile,
|
|
155
|
+
* otherwise one cached `createMcpResourceAuth` instance configured from
|
|
156
|
+
* `SMRT_MCP_RESOURCE`, `SMRT_MCP_ISSUER`, `SMRT_MCP_JWKS_URI` and the
|
|
157
|
+
* whitespace-separated `SMRT_MCP_SCOPES`. Missing configuration throws (fail
|
|
158
|
+
* closed); a failed construction is not cached, so a corrected environment
|
|
159
|
+
* is picked up on the next request. Pass the thunk as the `auth` option of
|
|
160
|
+
* `mountMcpRoute`/`mountMcpAppRoute` and to
|
|
161
|
+
* `mountMcpProtectedResourceMetadataRoute`.
|
|
162
|
+
*/
|
|
163
|
+
export declare function createHostedMcpResourceAuth(options: HostedMcpResourceAuthOptions): () => McpResourceAuth | null;
|
|
52
164
|
export declare function createMcpResourceAuth(options: McpResourceAuthOptions): McpResourceAuth;
|
|
53
165
|
//# sourceMappingURL=auth.d.ts.map
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,OAAO,EAAsB,KAAK,UAAU,EAAa,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/auth.js
CHANGED
|
@@ -697,7 +697,44 @@ function createRemoteJWKSet(url, options) {
|
|
|
697
697
|
});
|
|
698
698
|
}
|
|
699
699
|
//#endregion
|
|
700
|
+
//#region src/own-data.ts
|
|
701
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
|
|
702
|
+
"__proto__",
|
|
703
|
+
"constructor",
|
|
704
|
+
"prototype"
|
|
705
|
+
]);
|
|
706
|
+
function readOwnFields(value, keys) {
|
|
707
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
708
|
+
const prototype = Object.getPrototypeOf(value);
|
|
709
|
+
if (prototype !== Object.prototype && prototype !== null) return null;
|
|
710
|
+
for (const key of Reflect.ownKeys(value)) if (typeof key === "string" && FORBIDDEN_KEYS.has(key)) return null;
|
|
711
|
+
const fields = /* @__PURE__ */ Object.create(null);
|
|
712
|
+
for (const key of keys) {
|
|
713
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
714
|
+
if (!descriptor) continue;
|
|
715
|
+
if (!("value" in descriptor)) return null;
|
|
716
|
+
fields[key] = descriptor.value;
|
|
717
|
+
}
|
|
718
|
+
return fields;
|
|
719
|
+
}
|
|
720
|
+
function ownStringArray(value) {
|
|
721
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) return null;
|
|
722
|
+
const length = value.length;
|
|
723
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
724
|
+
if (key === "length") continue;
|
|
725
|
+
if (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length) return null;
|
|
726
|
+
}
|
|
727
|
+
const copy = [];
|
|
728
|
+
for (let index = 0; index < length; index += 1) {
|
|
729
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, index);
|
|
730
|
+
if (!descriptor || !("value" in descriptor) || typeof descriptor.value !== "string") return null;
|
|
731
|
+
copy.push(descriptor.value);
|
|
732
|
+
}
|
|
733
|
+
return copy;
|
|
734
|
+
}
|
|
735
|
+
//#endregion
|
|
700
736
|
//#region src/auth.ts
|
|
737
|
+
var TENANT_BINDINGS = ["direct", "direct-or-inherited"];
|
|
701
738
|
var scopePattern = /^[\x21\x23-\x5b\x5d-\x7e]+$/u;
|
|
702
739
|
function validateUrl(value, profile) {
|
|
703
740
|
let url;
|
|
@@ -717,6 +754,99 @@ function validateUrl(value, profile) {
|
|
|
717
754
|
function validString(value) {
|
|
718
755
|
return typeof value === "string" && value.length > 0 && value.length <= 1024 && value.trim() === value && [...value].every((character) => character.charCodeAt(0) > 31 && character.charCodeAt(0) !== 127);
|
|
719
756
|
}
|
|
757
|
+
var LOCAL_BEARER_PATTERN = /^Bearer +([A-Za-z0-9._~+/-]{16,512}=*)$/iu;
|
|
758
|
+
function createLocalMcpTokenAuth(options) {
|
|
759
|
+
const verify = options?.verify;
|
|
760
|
+
if (typeof verify !== "function") throw new TypeError("Local MCP token auth requires a verify function.");
|
|
761
|
+
const deny = () => ({
|
|
762
|
+
ok: false,
|
|
763
|
+
response: new Response(null, {
|
|
764
|
+
status: 401,
|
|
765
|
+
headers: {
|
|
766
|
+
"WWW-Authenticate": "Bearer error=\"invalid_token\"",
|
|
767
|
+
"Cache-Control": "no-store"
|
|
768
|
+
}
|
|
769
|
+
})
|
|
770
|
+
});
|
|
771
|
+
return Object.freeze({
|
|
772
|
+
metadataUrl: "",
|
|
773
|
+
sessionFallback: true,
|
|
774
|
+
metadataResponse: () => new Response(null, {
|
|
775
|
+
status: 404,
|
|
776
|
+
headers: { "Cache-Control": "no-store" }
|
|
777
|
+
}),
|
|
778
|
+
async authenticate(request) {
|
|
779
|
+
const match = LOCAL_BEARER_PATTERN.exec(request.headers.get("authorization") ?? "");
|
|
780
|
+
if (!match) return deny();
|
|
781
|
+
let principal;
|
|
782
|
+
try {
|
|
783
|
+
principal = await verify(match[1]);
|
|
784
|
+
} catch {
|
|
785
|
+
return deny();
|
|
786
|
+
}
|
|
787
|
+
const fields = readOwnFields(principal, [
|
|
788
|
+
"id",
|
|
789
|
+
"tenantId",
|
|
790
|
+
"kind",
|
|
791
|
+
"scopes"
|
|
792
|
+
]);
|
|
793
|
+
const scopes = fields ? ownStringArray(fields.scopes) : null;
|
|
794
|
+
if (!fields || !scopes || !validString(fields.id) || !validString(fields.tenantId) || fields.kind !== void 0 && !validString(fields.kind) || !scopes.every((scope) => scopePattern.test(scope))) return deny();
|
|
795
|
+
return {
|
|
796
|
+
ok: true,
|
|
797
|
+
principal: {
|
|
798
|
+
id: fields.id,
|
|
799
|
+
tenantId: fields.tenantId,
|
|
800
|
+
kind: fields.kind,
|
|
801
|
+
scopes,
|
|
802
|
+
tenantBinding: "direct"
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
var HOSTED_MCP_AUTH_ENV = Object.freeze({
|
|
809
|
+
resource: "SMRT_MCP_RESOURCE",
|
|
810
|
+
issuer: "SMRT_MCP_ISSUER",
|
|
811
|
+
jwksUri: "SMRT_MCP_JWKS_URI",
|
|
812
|
+
scopes: "SMRT_MCP_SCOPES"
|
|
813
|
+
});
|
|
814
|
+
function createHostedMcpResourceAuth(options) {
|
|
815
|
+
let cached;
|
|
816
|
+
let local;
|
|
817
|
+
const runtime = options.runtime;
|
|
818
|
+
return () => {
|
|
819
|
+
const profile = typeof options.profile === "function" ? options.profile() : options.profile;
|
|
820
|
+
if (profile === "local") {
|
|
821
|
+
const verify = runtime?.verifyLocalMcpToken;
|
|
822
|
+
if (typeof verify !== "function") return null;
|
|
823
|
+
local ??= createLocalMcpTokenAuth({ verify: (token) => verify.call(runtime, token) });
|
|
824
|
+
return local;
|
|
825
|
+
}
|
|
826
|
+
if (cached) return cached;
|
|
827
|
+
const env = options.env ?? globalThis.process?.env ?? {};
|
|
828
|
+
const required = (name) => {
|
|
829
|
+
const value = env[name]?.trim();
|
|
830
|
+
if (!value) throw new Error(`Hosted MCP requires ${name}.`);
|
|
831
|
+
return value;
|
|
832
|
+
};
|
|
833
|
+
const runtimeResolver = runtime?.resolveMcpPrincipal;
|
|
834
|
+
const resolvePrincipal = options.resolvePrincipal ?? (typeof runtimeResolver === "function" ? (identity) => runtimeResolver.call(runtime, identity) : void 0);
|
|
835
|
+
if (!resolvePrincipal) throw new Error("Hosted MCP requires an application-owned resolvePrincipal binding or a runtime with resolveMcpPrincipal.");
|
|
836
|
+
cached = createMcpResourceAuth({
|
|
837
|
+
profile,
|
|
838
|
+
resource: required(HOSTED_MCP_AUTH_ENV.resource),
|
|
839
|
+
issuer: required(HOSTED_MCP_AUTH_ENV.issuer),
|
|
840
|
+
jwksUri: required(HOSTED_MCP_AUTH_ENV.jwksUri),
|
|
841
|
+
scopes: required(HOSTED_MCP_AUTH_ENV.scopes).split(/\s+/u),
|
|
842
|
+
algorithms: options.algorithms ?? ["RS256"],
|
|
843
|
+
tokenType: options.tokenType,
|
|
844
|
+
requireTenant: options.requireTenant,
|
|
845
|
+
resolvePrincipal
|
|
846
|
+
});
|
|
847
|
+
return cached;
|
|
848
|
+
};
|
|
849
|
+
}
|
|
720
850
|
function createMcpResourceAuth(options) {
|
|
721
851
|
if (![
|
|
722
852
|
"local",
|
|
@@ -786,21 +916,29 @@ function createMcpResourceAuth(options) {
|
|
|
786
916
|
const granted = payload.scope === "" ? [] : payload.scope.split(" ");
|
|
787
917
|
if (granted.some((scope) => !scopePattern.test(scope))) return deny("invalid_token");
|
|
788
918
|
if (scopes.some((scope) => !granted.includes(scope))) return deny("insufficient_scope");
|
|
789
|
-
const
|
|
919
|
+
const fields = readOwnFields(await resolvePrincipal(Object.freeze({
|
|
790
920
|
issuer,
|
|
791
921
|
subject: payload.sub,
|
|
792
922
|
scopes: Object.freeze([...granted]),
|
|
793
923
|
claims: Object.freeze(payload)
|
|
794
|
-
}))
|
|
795
|
-
|
|
924
|
+
})), [
|
|
925
|
+
"id",
|
|
926
|
+
"tenantId",
|
|
927
|
+
"kind",
|
|
928
|
+
"roles",
|
|
929
|
+
"tenantBinding"
|
|
930
|
+
]);
|
|
931
|
+
const roles = fields?.roles === void 0 ? void 0 : ownStringArray(fields.roles);
|
|
932
|
+
if (!fields || roles === null || !validString(fields.id) || requireTenant && !validString(fields.tenantId) || fields.tenantId !== void 0 && !validString(fields.tenantId) || fields.kind !== void 0 && !validString(fields.kind) || roles !== void 0 && !roles.every(validString) || fields.tenantBinding !== void 0 && (typeof fields.tenantBinding !== "string" || !TENANT_BINDINGS.includes(fields.tenantBinding))) return deny("invalid_token");
|
|
796
933
|
return {
|
|
797
934
|
ok: true,
|
|
798
935
|
principal: {
|
|
799
|
-
id:
|
|
800
|
-
tenantId:
|
|
801
|
-
kind:
|
|
802
|
-
roles
|
|
803
|
-
scopes: [...granted]
|
|
936
|
+
id: fields.id,
|
|
937
|
+
tenantId: fields.tenantId,
|
|
938
|
+
kind: fields.kind,
|
|
939
|
+
roles,
|
|
940
|
+
scopes: [...granted],
|
|
941
|
+
tenantBinding: fields.tenantBinding ?? "direct-or-inherited"
|
|
804
942
|
}
|
|
805
943
|
};
|
|
806
944
|
} catch {
|
|
@@ -810,6 +948,6 @@ function createMcpResourceAuth(options) {
|
|
|
810
948
|
};
|
|
811
949
|
}
|
|
812
950
|
//#endregion
|
|
813
|
-
export { createMcpResourceAuth };
|
|
951
|
+
export { HOSTED_MCP_AUTH_ENV, createHostedMcpResourceAuth, createLocalMcpTokenAuth, createMcpResourceAuth };
|
|
814
952
|
|
|
815
953
|
//# sourceMappingURL=auth.js.map
|