access-token-authz.mjs
65 lines 2.3 KB
Raw
sha256:baa800aedf841dbf32081aa7b2befa288ac33dfc7175ac55014c55c4d8c742f9 docs: move durable-auth freeze/evidence to local developmen… Human 11 days ago
1 /**
2 * Scope-aware REST authorization for Hub access tokens.
3 *
4 * Web-session JWTs (no `type: mcp_access`) identify the caller by `sub`; role/scopes are
5 * derived elsewhere (`roleForSub` → `scopesForRole`). MCP OAuth access tokens carry an
6 * explicit `scopes` claim and must not be elevated by role lookup (confused-deputy /
7 * scope-elevation guard — docs/DURABLE-AGENT-AUTH-SPEC.md §8).
8 */
9
10 /**
11 * HTTP methods that never mutate resource state.
12 * @param {string} method
13 * @returns {boolean}
14 */
15 export function isSafeHttpMethod(method) {
16 const m = String(method || 'GET').toUpperCase();
17 return m === 'GET' || m === 'HEAD' || m === 'OPTIONS';
18 }
19
20 /**
21 * Whether an MCP access-token scope list permits the HTTP method on REST.
22 * @param {string[]} scopes
23 * @param {string} method
24 * @returns {boolean}
25 */
26 export function mcpScopesPermitMethod(scopes, method) {
27 const list = Array.isArray(scopes) ? scopes : [];
28 const hasWrite =
29 list.includes('vault:write') || list.includes('vault:admin') || list.includes('admin');
30 const hasRead =
31 hasWrite || list.includes('vault:read');
32 if (isSafeHttpMethod(method)) return hasRead;
33 return hasWrite;
34 }
35
36 /**
37 * Resolve `sub` from a verified access-token payload for a REST request method.
38 * Returns null when the token is missing, invalid for identity, or (for mcp_access)
39 * insufficient for the method.
40 *
41 * @param {object|null|undefined} payload - decoded JWT payload (already verified)
42 * @param {{ method?: string }} [opts]
43 * @returns {string|null}
44 */
45 export function subFromVerifiedPayload(payload, opts = {}) {
46 if (!payload || typeof payload !== 'object') return null;
47 const sub = typeof payload.sub === 'string' ? payload.sub : null;
48 if (!sub) return null;
49 if (payload.type === 'mcp_access') {
50 if (!mcpScopesPermitMethod(payload.scopes, opts.method || 'GET')) return null;
51 }
52 return sub;
53 }
54
55 /**
56 * Whether durable MCP / native OAuth agent-auth endpoints may mount.
57 * Offline-locked posture and Netlify serverless both leave them unmounted
58 * (docs/DURABLE-AGENT-AUTH-SPEC.md §14).
59 *
60 * @param {{ sessionSecret?: string|null, netlify?: boolean, offlineLockedActive?: boolean }} opts
61 * @returns {boolean}
62 */
63 export function shouldMountDurableAgentAuth(opts = {}) {
64 return Boolean(opts.sessionSecret) && !opts.netlify && !opts.offlineLockedActive;
65 }
File History 1 commit
sha256:baa800aedf841dbf32081aa7b2befa288ac33dfc7175ac55014c55c4d8c742f9 docs: move durable-auth freeze/evidence to local developmen… Human 11 days ago