mcp-authz 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ //#region src/cli.d.ts
2
+ declare function main(argv: readonly string[]): number | Promise<number>;
3
+ //#endregion
4
+ export { main };
package/dist/cli.js ADDED
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ import { i as reconcile, r as definePolicy } from "./policy-BBp3Jq6G.js";
3
+ import { readFileSync, writeFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { parseArgs } from "node:util";
7
+ //#region src/cli.ts
8
+ /**
9
+ * Two questions a policy file cannot answer by being read.
10
+ *
11
+ * `check` runs the same reconciliation the server runs at boot, without booting
12
+ * it, so CI catches drift on the pull request rather than on deploy. `explain`
13
+ * answers "why can Alice do this", which nothing else answers at all: the
14
+ * decision carries the permissions, and only `policy.explain` carries the rules
15
+ * that produced them.
16
+ *
17
+ * Deliberately no colour library and no argument parser. `node:util` has one,
18
+ * and a dependency here would be a dependency in every install of the package.
19
+ */
20
+ const USAGE = `mcp-authz — inspect a policy without running a server
21
+
22
+ mcp-authz check <policy.json> [--capabilities <map.json>]
23
+ mcp-authz record <connector.ts> [--out <permissions.ts>]
24
+ mcp-authz record --upstream <url> [--token <bearer>] [--out <permissions.ts>]
25
+ mcp-authz record <connector.ts|--upstream <url>> --check <permissions.ts>
26
+ mcp-authz explain <policy.json> --identity <identity.json>|- [--capabilities <map.json>]
27
+
28
+ Files
29
+ <policy.json> the object you would hand definePolicy
30
+ --capabilities a .json map, or a .ts/.js module exporting PERMISSIONS,
31
+ which is what record writes
32
+ --identity an Identity, or a decoded token payload (iss, sub, email,
33
+ email_verified, hd). Use - to read it from stdin.
34
+
35
+ Exit codes
36
+ 0 fine, warnings included
37
+ 1 the policy is invalid, or a capability no role can reach
38
+ `;
39
+ function main(argv) {
40
+ const { values, positionals } = parseArgs({
41
+ args: [...argv],
42
+ allowPositionals: true,
43
+ options: {
44
+ capabilities: { type: "string" },
45
+ identity: { type: "string" },
46
+ out: { type: "string" },
47
+ upstream: { type: "string" },
48
+ token: { type: "string" },
49
+ check: { type: "string" },
50
+ help: {
51
+ type: "boolean",
52
+ short: "h"
53
+ }
54
+ }
55
+ });
56
+ const [command, policyPath] = positionals;
57
+ if (values.help || !command) {
58
+ process.stdout.write(USAGE);
59
+ return values.help ? 0 : 1;
60
+ }
61
+ if (command === "record") {
62
+ if (values.upstream) return record({
63
+ upstream: values.upstream,
64
+ token: values.token
65
+ }, values.out, values.check);
66
+ if (!policyPath) {
67
+ process.stderr.write("record needs a module whose default export builds the server, or --upstream <url>.\n");
68
+ return 1;
69
+ }
70
+ return record({ module: policyPath }, values.out, values.check);
71
+ }
72
+ if (!policyPath) {
73
+ process.stderr.write(`${command} needs a path to a policy file.\n`);
74
+ return 1;
75
+ }
76
+ const policy = definePolicy(readJson(policyPath));
77
+ const run = (capabilities) => {
78
+ if (command === "check") return check(policy, capabilities);
79
+ if (command === "explain") {
80
+ if (!values.identity) {
81
+ process.stderr.write("explain needs --identity <file>, or - for stdin.\n");
82
+ return 1;
83
+ }
84
+ return explain(policy, identityFrom(readJson(values.identity)), capabilities);
85
+ }
86
+ process.stderr.write(`Unknown command '${command}'.\n\n${USAGE}`);
87
+ return 1;
88
+ };
89
+ if (!values.capabilities) return run(void 0);
90
+ if (values.capabilities.endsWith(".json")) return run(new Map(Object.entries(readJson(values.capabilities))));
91
+ return importCapabilities(values.capabilities).then(run);
92
+ }
93
+ /** Read the map out of a module `record` produced, or one written by hand. */
94
+ async function importCapabilities(path) {
95
+ const loaded = await import(pathToFileURL(resolve(path)).href);
96
+ const map = loaded.PERMISSIONS ?? loaded.default;
97
+ if (!map || typeof map !== "object") throw new Error(`${path} must export PERMISSIONS, or default, mapping each capability to a permission.`);
98
+ return new Map(Object.entries(map));
99
+ }
100
+ async function record(source, out, against) {
101
+ let toolkit;
102
+ try {
103
+ toolkit = await import("./testing.js");
104
+ } catch {
105
+ process.stderr.write("record needs @modelcontextprotocol/client, which is an optional peer.\n npm install -D @modelcontextprotocol/client\n");
106
+ return 1;
107
+ }
108
+ let capabilities;
109
+ if ("upstream" in source) capabilities = await toolkit.recordUpstream(source.upstream, { bearer: source.token });
110
+ else {
111
+ const loaded = await import(pathToFileURL(resolve(source.module)).href);
112
+ if (typeof loaded.default !== "function") {
113
+ process.stderr.write(`${source.module} must default-export a function that builds the server.\n`);
114
+ return 1;
115
+ }
116
+ capabilities = await toolkit.recordCapabilities(loaded.default);
117
+ }
118
+ if (against) return drift(capabilities, against);
119
+ const generated = toolkit.toPermissionsModule(capabilities);
120
+ if (out) writeFileSync(out, generated);
121
+ else process.stdout.write(generated);
122
+ return 0;
123
+ }
124
+ /**
125
+ * Compare what the server has now against the map somebody committed.
126
+ *
127
+ * The snapshot story needs a test runner, and the upstream path has none — this
128
+ * is what lets a URL-only server be watched from CI at all.
129
+ */
130
+ async function drift(live, path) {
131
+ const loaded = await import(pathToFileURL(resolve(path)).href);
132
+ const priced = Object.keys(loaded.PERMISSIONS ?? {});
133
+ const recorded = loaded.FINGERPRINTS ?? {};
134
+ const added = live.names.filter((name) => !priced.includes(name));
135
+ const removed = priced.filter((name) => !live.names.includes(name));
136
+ const changed = live.names.filter((name) => priced.includes(name) && recorded[name] && recorded[name] !== live.fingerprints[name]);
137
+ const unbaselined = Object.keys(recorded).length === 0 ? ` — ${path} carries no FINGERPRINTS, so definitions were not compared; re-record to add one` : "";
138
+ if (added.length === 0 && removed.length === 0 && changed.length === 0) {
139
+ process.stdout.write(`${live.names.length} capabilities, names unchanged since ${path}${unbaselined}\n`);
140
+ return 0;
141
+ }
142
+ const lines = ["The server no longer matches the recorded capabilities:", ""];
143
+ for (const name of added) lines.push(` + ${name}`, " never priced, so nobody decided who may reach it");
144
+ for (const name of removed) lines.push(` - ${name}`, " priced here, but the server no longer offers it");
145
+ for (const name of changed) lines.push(` ~ ${name}`, " same name, different definition than the one recorded");
146
+ if (unbaselined) lines.push("", `Note:${unbaselined.slice(3)}`);
147
+ lines.push("", "Re-record when the change is expected, and review the diff.", "");
148
+ process.stdout.write(lines.join("\n"));
149
+ return 1;
150
+ }
151
+ function check(policy, capabilities) {
152
+ const lines = [`${policy.roles.size} role${policy.roles.size === 1 ? "" : "s"}`, `${policy.permissions.length} permission${policy.permissions.length === 1 ? "" : "s"}`];
153
+ if (capabilities) lines.push(`${capabilities.size} capabilities`);
154
+ for (const line of lines) process.stdout.write(` ok ${line}\n`);
155
+ if (!capabilities) {
156
+ process.stdout.write("\nPass --capabilities to reconcile the policy against the tools that use it.\n");
157
+ return 0;
158
+ }
159
+ const { error, warning } = reconcile(policy.roles, capabilities);
160
+ if (warning) process.stdout.write(`\n${warning}\n`);
161
+ if (error) {
162
+ process.stderr.write(`\n${error}\n`);
163
+ return 1;
164
+ }
165
+ return 0;
166
+ }
167
+ function explain(policy, identity, capabilities) {
168
+ const { principal, matched, deniedBy } = policy.explain(identity);
169
+ process.stdout.write(`${identity.email ?? identity.sub}\n\n`);
170
+ process.stdout.write("Matched rules\n");
171
+ if (matched.length === 0) process.stdout.write(" none, so this caller holds nothing\n");
172
+ for (const rule of matched) {
173
+ const verdict = rule.deny ? "DENY" : rule.roles.join(", ");
174
+ process.stdout.write(` rule ${rule.index} ${describe(rule)} -> ${verdict}\n`);
175
+ }
176
+ if (deniedBy) process.stdout.write(`\nRule ${deniedBy.index} denies, which empties the grant whatever else matched.\n`);
177
+ process.stdout.write(`\nEffective roles\n ${list(principal.roles)}\n`);
178
+ const permissions = principal.permissions.map((p) => p === "*" ? "* (every permission)" : p);
179
+ process.stdout.write(`\nPermissions\n ${list(permissions)}\n`);
180
+ if (capabilities) {
181
+ const usable = [...capabilities].filter(([, permission]) => principal.can(permission)).map(([capability]) => capability);
182
+ process.stdout.write(`\nCapabilities\n ${list(usable)}\n`);
183
+ }
184
+ return 0;
185
+ }
186
+ /** Every field of a match, so a rule that matched on two things reads as two. */
187
+ function describe(rule) {
188
+ const parts = Object.entries(rule.match).flatMap(([field, value]) => field === "claim" ? Object.entries(value).map(([path, want]) => `${path}=${want}`) : [`${field}=${String(value)}`]);
189
+ return parts.length > 0 ? parts.join(" and ") : "every caller";
190
+ }
191
+ function list(values) {
192
+ return values.length > 0 ? values.join("\n ") : "none";
193
+ }
194
+ /**
195
+ * An Identity as written, or a decoded token payload.
196
+ *
197
+ * This mirrors what the verifier produces, for a caller who has not been
198
+ * verified: `explain` answers a what-if, so it trusts the file it was handed.
199
+ */
200
+ function identityFrom(value) {
201
+ if (typeof value !== "object" || value === null) throw new Error("Identity must be an object.");
202
+ const raw = value;
203
+ const issuer = str(raw.issuer) ?? str(raw.iss);
204
+ const sub = str(raw.sub);
205
+ if (!issuer || !sub) throw new Error("Identity needs an issuer ('iss') and a subject ('sub').");
206
+ return {
207
+ issuer,
208
+ sub,
209
+ email: str(raw.email),
210
+ emailVerified: raw.emailVerified === true || raw.email_verified === true,
211
+ domain: str(raw.domain) ?? str(raw.hd),
212
+ claims: typeof raw.claims === "object" && raw.claims !== null ? raw.claims : raw
213
+ };
214
+ }
215
+ function str(value) {
216
+ return typeof value === "string" && value.length > 0 ? value : void 0;
217
+ }
218
+ function readJson(path) {
219
+ return JSON.parse(readFileSync(path === "-" ? 0 : path, "utf8"));
220
+ }
221
+ if (import.meta.url === `file://${process.argv[1]}`) try {
222
+ process.exitCode = await main(process.argv.slice(2));
223
+ } catch (error) {
224
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
225
+ process.exitCode = 1;
226
+ }
227
+ //#endregion
228
+ export { main };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
- import { a as Policy, c as Rule, d as definePolicy, f as reconcile, h as Identity, i as PermissionOf, l as createPrincipal, m as DenialReason, n as Match, o as PolicySpec, p as AccessDeniedError, r as PermissionCatalog, s as Principal, u as definePermissions } from "./policy-CnQj53Hq.js";
2
- import { AuthInfo, CallToolResult, GetPromptResult, Icon, Implementation, InboundClassificationOutcome, McpServer, OAuthMetadata, OAuthTokenVerifier, ReadResourceResult, ResourceMetadata, ResourceTemplate, ServerOptions as ServerOptions$1, StandardSchemaV1, StandardSchemaWithJSON, ToolAnnotations } from "@modelcontextprotocol/server";
1
+ import { _ as Identity, a as PermissionCatalog, c as PolicySpec, d as createPrincipal, f as definePermissions, g as DenialReason, h as AccessDeniedError, i as MatchedRule, l as Principal, m as reconcile, n as Explanation, o as PermissionOf, p as definePolicy, r as Match, s as Policy, u as Rule } from "./policy-DuZbwrKf.js";
2
+ import { a as AuditDeliveryFailure, c as AuditSink, d as PromptConfig, f as ResourceConfig, h as authz, i as ApprovalSink, l as Capability, m as ToolConfig, n as ApprovalRefusedError, o as AuditErrorSink, p as ServerOptions, r as ApprovalRequest, s as AuditEvent, t as ApprovalDecision, u as Definition } from "./tools-BQE1O-7P.js";
3
+ import { a as AuthorizationDecisionSink, i as AuthorizationDecisionEvent, n as identityFromAuth, r as jwksVerifier, t as VerifierOptions } from "./verifier-DF6gUMQ6.js";
4
+ import { a as scopesForCapability, i as decodeMcpNameHeader, n as ScopeRequirement, o as scopesFromMcpHeaders, r as ToolScopeMap, s as TrustedMcpRoute, t as CapabilityScopeMap } from "./ladder-D18eJ7tD.js";
5
+ import { AuthInfo, McpServer, OAuthMetadata, OAuthTokenVerifier } from "@modelcontextprotocol/server";
3
6
  //#region src/discovery.d.ts
4
7
  type DiscoverOptions = {
5
8
  /** Swap in for tests, or to add a timeout or proxy. Defaults to global fetch. */
@@ -7,125 +10,6 @@ type DiscoverOptions = {
7
10
  };
8
11
  declare function discoverOAuth(issuer: string, options?: DiscoverOptions): Promise<OAuthMetadata>;
9
12
  //#endregion
10
- //#region src/tools.d.ts
11
- /**
12
- * Tools, prompts and resources that carry the permission they require.
13
- *
14
- * Declaring it here rather than in a separate rules file is the whole point: it
15
- * is checked against the policy by the compiler, reconciled against the policy
16
- * at boot, and it cannot drift when somebody renames the tool.
17
- *
18
- * MCP exposes three things a caller can reach, so gating only tools leaves two
19
- * doors open. All three register the same way: check the permission, skip the
20
- * registration when the caller lacks it, audit what they do reach.
21
- */
22
- /** Which of the three a caller reached. */
23
- type Capability = 'tool' | 'prompt' | 'resource';
24
- /** What actually happened, for the audit log the downstream API cannot write. */
25
- type AuditEvent = {
26
- issuer: string;
27
- sub: string;
28
- email?: string;
29
- kind: Capability;
30
- /** The registered name, e.g. `update_case`. */
31
- name: string;
32
- permission: string;
33
- /** Whatever the definition's own `audit` said the call touched, e.g. `case:C1234`. */
34
- resource?: string;
35
- decision: 'allow';
36
- phase: 'attempt' | 'success' | 'failure';
37
- at: string;
38
- durationMs?: number;
39
- error?: string;
40
- };
41
- type AuditSink = (event: AuditEvent) => unknown | Promise<unknown>;
42
- type InferArgs<S> = S extends StandardSchemaV1<unknown, infer Output> ? Output : undefined;
43
- /** Shared by all three: what it costs, and what the call touched. */
44
- type Gated<P extends string, A> = {
45
- /** Required to reach it. Typed to the policy's permissions, so a typo is a build error. */
46
- permission: P;
47
- /**
48
- * Name the thing the call touched, for the audit event. The library cannot
49
- * know that `{ caseId: 'C1234' }` means a case; the application always does.
50
- */
51
- audit?: (args: A) => string | undefined;
52
- };
53
- type ToolConfig<P extends string, S> = Gated<P, InferArgs<S>> & {
54
- title?: string;
55
- description?: string;
56
- inputSchema?: S;
57
- outputSchema?: StandardSchemaWithJSON;
58
- annotations?: ToolAnnotations;
59
- icons?: Icon[];
60
- };
61
- type PromptConfig<P extends string, S> = Gated<P, InferArgs<S>> & {
62
- title?: string;
63
- description?: string;
64
- argsSchema?: S;
65
- icons?: Icon[];
66
- };
67
- type ResourceConfig<P extends string> = Gated<P, URL> & ResourceMetadata & {
68
- /** The URI clients read, or a template for a family of them. */
69
- uri: string | ResourceTemplate;
70
- };
71
- /**
72
- * Erased once built, so a server registers every capability the same way
73
- * whatever it accepts.
74
- */
75
- type Definition<P extends string, C = Principal<P>> = {
76
- /**
77
- * Unique key for the boot-time check. Bare for a tool, prefixed otherwise, so
78
- * a prompt sharing a tool's name stays a separate entry rather than shadowing
79
- * it.
80
- */
81
- label: string;
82
- kind: Capability;
83
- /** Exact protocol name used to refuse an unpermitted direct invocation before scope step-up. */
84
- routeName?: string;
85
- routeMatches?: (name: string) => boolean;
86
- permission: P;
87
- register: (server: McpServer, principal: Principal<P>, context: C, onAudit?: AuditSink) => void;
88
- };
89
- type ServerOptions = Implementation & {
90
- /** Called on every permitted invocation. The one record tying a person to an action. */
91
- onAudit?: AuditSink;
92
- /** Pass-through SDK options; declared gated capabilities are merged in. */
93
- mcp?: ServerOptions$1;
94
- };
95
- /**
96
- * A per-request server factory over a fixed set of capabilities, plus the
97
- * label-to-permission map that `createMcpFetch` reconciles against the policy
98
- * at boot.
99
- */
100
- type ServerFactory<C> = ((context: C) => McpServer) & {
101
- permissions: ReadonlyMap<string, string>;
102
- routePermissions: ReadonlyMap<string, string>;
103
- permissionForRoute: (kind: Capability, name: string) => string | undefined;
104
- };
105
- /**
106
- * Everything bound to one policy: `permission` accepts only what that policy can
107
- * grant, in all four places, from one call.
108
- *
109
- * The policy is a type carrier here and is never invoked. Authorization happens
110
- * once per request when the principal is resolved, not once per definition.
111
- *
112
- * ```ts
113
- * const { tool, prompt, resource, server } = authz(policy);
114
- * ```
115
- */
116
- declare function bindAuthz<P extends string, C, H>(_permissions: Policy<P> | PermissionCatalog<P>, principalOf: (context: C) => Principal<P>, handlerContext: (context: C, principal: Principal<P>) => H): {
117
- tool: <S extends StandardSchemaWithJSON | undefined = undefined>(name: string, config: ToolConfig<P, S>, handler: (args: InferArgs<S>, context: H) => CallToolResult | Promise<CallToolResult>) => Definition<P, C>;
118
- prompt: <S extends StandardSchemaWithJSON | undefined = undefined>(name: string, config: PromptConfig<P, S>, handler: (args: InferArgs<S>, context: H) => GetPromptResult | Promise<GetPromptResult>) => Definition<P, C>;
119
- resource: (name: string, config: ResourceConfig<P>, handler: (uri: URL, context: H) => ReadResourceResult | Promise<ReadResourceResult>) => Definition<P, C>;
120
- server: (definitions: readonly Definition<P, C>[], options: ServerOptions) => ServerFactory<C>;
121
- };
122
- declare function authz<P extends string>(policy: Policy<P> | PermissionCatalog<P>): ReturnType<typeof bindAuthz<P, Principal<P>, {
123
- principal: Principal<P>;
124
- }>>;
125
- declare function authz<P extends string, C>(policy: Policy<P> | PermissionCatalog<P>, options: {
126
- principal: (context: C) => Principal<P>;
127
- }): ReturnType<typeof bindAuthz<P, C, C>>;
128
- //#endregion
129
13
  //#region src/gate.d.ts
130
14
  /**
131
15
  * Gate a server somebody else builds.
@@ -154,102 +38,31 @@ declare function authz<P extends string, C>(policy: Policy<P> | PermissionCatalo
154
38
  type GateOptions = {
155
39
  /** Same event as `authz`, for tools this package did not define. */
156
40
  onAudit?: AuditSink;
41
+ /** Names this deployment on every event it emits. See `ServerOptions`. */
42
+ emitter?: string;
43
+ /** Receives a failed terminal audit write without changing the completed action's result. */
44
+ onAuditError?: AuditErrorSink;
157
45
  /**
158
46
  * Name the thing a call touched, keyed by the same label as `permissions`.
159
47
  * Their tool, your domain knowledge.
160
48
  */
161
49
  audit?: Readonly<Record<string, (args: never) => string | undefined>>;
50
+ /**
51
+ * Ask a person before one of their tools runs, keyed by the same label. The
52
+ * one thing worth adding to somebody else's destructive tool without touching
53
+ * their code.
54
+ */
55
+ approval?: Readonly<Record<string, boolean | ((args: never) => boolean)>>;
56
+ /** Awaited human decision. Required when `approval` names anything. */
57
+ onApproval?: ApprovalSink;
58
+ /** Silence is a refusal after this long. Defaults to 45s, as in `authz`. */
59
+ approvalTimeoutMs?: number;
162
60
  };
163
61
  /** Bare name for a tool, `prompt:`/`resource:` prefixed for the rest. */
164
62
  type PermissionMap<P extends string> = Readonly<Record<string, P>> | ReadonlyMap<string, P>;
165
63
  declare function gate<P extends string>(server: McpServer, principal: Principal<P>, permissions: PermissionMap<P>, options?: GateOptions): McpServer;
166
64
  //#endregion
167
- //#region src/routing.d.ts
168
- type TrustedMcpRoute = {
169
- kind: 'modern';
170
- body: unknown;
171
- outcome: Extract<InboundClassificationOutcome, {
172
- kind: 'modern';
173
- }>;
174
- method: string;
175
- name?: string;
176
- };
177
- //#endregion
178
- //#region src/scopes.d.ts
179
- /**
180
- * Helpers for deriving required OAuth scopes from Streamable HTTP headers
181
- * (SEP-2243). Use with `createMcpFetch({ scopesForRequest })`.
182
- */
183
- type ScopeRequirement = string | readonly string[];
184
- type ToolScopeMap = Readonly<Record<string, ScopeRequirement>>;
185
- type CapabilityScopeMap = ToolScopeMap;
186
- /**
187
- * Tools use their bare name (or `tool:name`); prompts use `prompt:name`; and
188
- * resources use `resource:<uri>`. Everything else needs only the baseline.
189
- */
190
- declare function scopesFromMcpHeaders(request: Request, toolScopes: ToolScopeMap, baseline?: string): string[];
191
- /** Select scopes from an already validated MCP method/name pair. */
192
- declare function scopesForCapability(method: string | undefined, name: string | undefined, capabilityScopes: CapabilityScopeMap, baseline?: string): string[];
193
- /** Decode SEP-2243's optional Base64 sentinel without accepting non-canonical input. */
194
- declare function decodeMcpNameHeader(value: string): string | undefined;
195
- //#endregion
196
- //#region src/verifier.d.ts
197
- /**
198
- * Token verification, the one thing the SDK deliberately leaves to you.
199
- *
200
- * `authInfo` is strictly pass-through in the SDK: it is never derived from
201
- * request headers, and no token is checked unless we check it. So this file is
202
- * the security boundary of the whole server.
203
- *
204
- * The authorization server is something that already exists (WorkOS, Stytch,
205
- * Auth0) doing dynamic client registration / CIMD and Google Workspace login.
206
- * Google cannot play that role itself: it has no DCR/CIMD, and it will not
207
- * mint a token whose audience is this server.
208
- */
209
- type VerifierOptions = {
210
- /** The AS issuer, e.g. `https://auth.acme.com`. Must match the token's `iss`. */
211
- issuer: string;
212
- /** Where the AS publishes its signing keys. */
213
- jwksUri: string;
214
- /**
215
- * This server's public URL. A token minted for a different resource is
216
- * refused even when its signature is valid (RFC 8707).
217
- */
218
- resource: URL;
219
- /**
220
- * Restrict to one Google Workspace domain, checked against the `hd` claim
221
- * the AS passes through. Omit to accept any domain the AS admits.
222
- */
223
- allowedDomain?: string;
224
- /** Claim carrying the verified email. Auth0 and WorkOS both use `email`. */
225
- emailClaim?: string;
226
- /** Claim proving the email was verified. Defaults to `email_verified`. */
227
- emailVerifiedClaim?: string;
228
- /** Require an explicit `true` verified-email claim. Defaults to true. */
229
- requireEmailVerified?: boolean;
230
- };
231
- /**
232
- * A JWKS-backed verifier. Keys are fetched once and cached by `jose`, which
233
- * also handles rotation, so a key roll at the AS does not need a redeploy.
234
- */
235
- declare function jwksVerifier(options: VerifierOptions): OAuthTokenVerifier & {
236
- identityOf: (auth: AuthInfo) => Identity;
237
- };
238
- /** Default identity mapper for custom verifiers using `AuthInfo.extra`. */
239
- declare function identityFromAuth(auth: AuthInfo): Identity;
240
- //#endregion
241
65
  //#region src/handler.d.ts
242
- type AuthorizationDecisionEvent = {
243
- issuer: string;
244
- sub: string;
245
- email?: string;
246
- decision: 'allow' | 'deny';
247
- roles: readonly string[];
248
- permissions: readonly string[];
249
- reason?: string;
250
- at: string;
251
- };
252
- type AuthorizationDecisionSink = (event: AuthorizationDecisionEvent) => unknown | Promise<unknown>;
253
66
  /**
254
67
  * An MCP Streamable HTTP resource server (2026-07-28) with
255
68
  * each person's own Google login in front of a per-request server factory.
@@ -325,6 +138,13 @@ type McpFetchOptions<TContext, P extends string = string> = {
325
138
  authorize?: (identity: Identity) => Promise<Principal<P>> | Principal<P>;
326
139
  /** Awaited access-decision sink. A rejection fails closed. */
327
140
  onDecision?: AuthorizationDecisionSink;
141
+ /**
142
+ * Names this deployment on every event it emits.
143
+ *
144
+ * Set the same value here and in `server(...)`, or a dashboard reading both
145
+ * cannot tell that a refusal and a call came from the same place.
146
+ */
147
+ emitter?: string;
328
148
  /**
329
149
  * After auth: map the verified identity to backend context, or throw
330
150
  * `AccessDeniedError` for an actionable 403. Omit to use `policy` alone.
@@ -341,6 +161,7 @@ type McpFetchOptions<TContext, P extends string = string> = {
341
161
  permissions?: ReadonlyMap<string, string>;
342
162
  /** Protocol route to permission, used to distinguish policy denial from scope step-up. */
343
163
  routePermissions?: ReadonlyMap<string, string>;
164
+ routeNameFor?: (kind: 'tool' | 'prompt' | 'resource', name: string) => string | undefined;
344
165
  /** Resolve exact and templated protocol routes to their declared permission. */
345
166
  permissionForRoute?: (kind: 'tool' | 'prompt' | 'resource', name: string) => string | undefined;
346
167
  };
@@ -357,8 +178,9 @@ type McpFetchOptions<TContext, P extends string = string> = {
357
178
  /** Reject 2025-era requests by default; opt in only when legacy clients are required. */
358
179
  legacy?: 'reject' | 'stateless';
359
180
  /**
360
- * Cap on a body read before authentication, which only the declarative scope
361
- * path does. Defaults to 1 MiB; over it is a 413.
181
+ * Cap on the body read that names the capability. Defaults to 1 MiB; over it
182
+ * is a 413. The read happens after the bearer gate, so the cap bounds an
183
+ * authenticated caller rather than anyone who can reach the port.
362
184
  */
363
185
  maxRequestBytes?: number;
364
186
  };
@@ -383,4 +205,4 @@ declare function createMcpFetch<TContext = Principal<string>, P extends string =
383
205
  resolve: (identity: Identity, principal: undefined) => Promise<TContext> | TContext;
384
206
  }): (request: Request) => Promise<Response>;
385
207
  //#endregion
386
- export { AccessDeniedError, type AuditEvent, type AuditSink, type AuthorizationDecisionEvent, type AuthorizationDecisionSink, type Capability, type CapabilityScopeMap, type Definition, type DenialReason, type DiscoverOptions, type GateOptions, type Identity, type Match, type McpFetchOptions, type PermissionCatalog, type PermissionMap, type PermissionOf, type Policy, type PolicySpec, type Principal, type PromptConfig, type ResourceConfig, type Rule, type ScopeRequirement, type ServerOptions, type ToolConfig, type ToolScopeMap, type TrustedMcpRoute, type VerifierOptions, authz, createMcpFetch, createPrincipal, decodeMcpNameHeader, definePermissions, definePolicy, discoverOAuth, gate, identityFromAuth, jwksVerifier, reconcile, scopesForCapability, scopesFromMcpHeaders };
208
+ export { AccessDeniedError, type ApprovalDecision, ApprovalRefusedError, type ApprovalRequest, type ApprovalSink, type AuditDeliveryFailure, type AuditErrorSink, type AuditEvent, type AuditSink, type AuthorizationDecisionEvent, type AuthorizationDecisionSink, type Capability, type CapabilityScopeMap, type Definition, type DenialReason, type DiscoverOptions, type Explanation, type GateOptions, type Identity, type Match, type MatchedRule, type McpFetchOptions, type PermissionCatalog, type PermissionMap, type PermissionOf, type Policy, type PolicySpec, type Principal, type PromptConfig, type ResourceConfig, type Rule, type ScopeRequirement, type ServerOptions, type ToolConfig, type ToolScopeMap, type TrustedMcpRoute, type VerifierOptions, authz, createMcpFetch, createPrincipal, decodeMcpNameHeader, definePermissions, definePolicy, discoverOAuth, gate, identityFromAuth, jwksVerifier, reconcile, scopesForCapability, scopesFromMcpHeaders };