Skip to the content

Capsid menu

app/lib/mcp/scopes.ts

40 lines and 2 functions in DrDustinEdwards/carrel, at commit d5d5e82 on 2026-10-24. This file on GitHub.

Functions

The functions of scopes.ts
FunctionLinesComplexityCoveredCRAPMutation
grantScopes23 to 305100%OkCRAP 5No data: mutation testing has not run on this repo yet
effectiveScopes33 to 424100%OkCRAP 4No data: mutation testing has not run on this repo yet

Imports

It imports no other module of this repo. Imported by app/lib/mcp/door.ts, app/lib/mcp/tools.ts.

Source

// Scopes (ruling capsid/rulings/mcp-2026-10-10.md, rule 12.3). A client that asks for carrel:read
// alone gets read alone. A client that asks for carrel:write, or names no Carrel scope at all, gets
// read and write (Dustin, 2026-10-10: a client that names no scope must not lose drafts and flags),
// and the person can always narrow it to reading on the consent page. What a session may do is still
// decided by the person's role below this layer: a scope can only narrow that, never widen it.
//
// The one scope a grant carried before 2026-10-10, "carrel", was approved on a consent page that
// named writing, so it reads as both until that grant is replaced at the next sign-in.

export const SCOPE_READ = "carrel:read";
export const SCOPE_WRITE = "carrel:write";
const LEGACY_SCOPE = "carrel";

/** The scopes the authorization server offers, in the order the consent page names them. */
export const SCOPES_SUPPORTED = [SCOPE_READ, SCOPE_WRITE];

/**
 * What a new grant carries. Reading only, when the person chose it on the consent page or the client
 * asked for carrel:read and nothing more. Otherwise read and write: the client asked for write (or
 * for the legacy scope, which a client that connected before may still send), or it named no Carrel
 * scope at all, which defaults to both.
 */
export function grantScopes(requested: readonly string[], opts: { readOnly?: boolean } = {}): string[] {
  if (opts.readOnly) return [SCOPE_READ];
  const asked = new Set(requested);
  if (asked.has(SCOPE_WRITE) || asked.has(LEGACY_SCOPE)) return [SCOPE_READ, SCOPE_WRITE];
  // Naming no Carrel scope at all defaults to both.
  if (!asked.has(SCOPE_READ)) return [SCOPE_READ, SCOPE_WRITE];
  return [SCOPE_READ];
}

/** The scopes a token's grant carries, with the legacy scope read as both. */
export function effectiveScopes(granted: readonly string[]): string[] {
  const out = new Set<string>();
  for (const scope of granted) {
    if (scope === LEGACY_SCOPE) {
      out.add(SCOPE_READ);
      out.add(SCOPE_WRITE);
    } else if (SCOPES_SUPPORTED.includes(scope)) out.add(scope);
  }
  return [...out];
}

/** Every scope, for a named agent key: its reach is its People row's role, set by Dustin. */
export const AGENT_SCOPES = [SCOPE_READ, SCOPE_WRITE];