import type { Did, Nsid } from "@atcute/lexicons/syntax"; type RepoAction = "create" | "update" | "delete"; export type Permission = | { readonly resource: "repo"; readonly collection: Nsid; readonly actions: readonly RepoAction[]; } | { readonly resource: "rpc"; readonly lxm: Nsid; readonly aud: Did }; const ANY = "*"; const ALL_ACTIONS: readonly RepoAction[] = ["create", "update", "delete"]; interface Grant { readonly resource: string; readonly positional: string | null; readonly params: URLSearchParams; } const grantFrom = (scope: string): Grant => { const query = scope.indexOf("?"); const head = query < 0 ? scope : scope.slice(0, query); const params = new URLSearchParams(query < 0 ? "" : scope.slice(query + 1)); const colon = head.indexOf(":"); return colon < 0 ? { resource: head, positional: null, params } : { resource: head.slice(0, colon), positional: head.slice(colon + 1), params }; }; const listOf = (grant: Grant, name: string): readonly string[] => grant.positional === null ? grant.params.getAll(name) : grant.positional === "" ? [] : [grant.positional]; const isRepoAction = (value: string): value is RepoAction => (ALL_ACTIONS as readonly string[]).includes(value); const nameMatches = (granted: string, wanted: Nsid): boolean => granted === ANY || granted === wanted; const covers = (grant: Grant, wanted: Permission): boolean => { if (grant.resource !== wanted.resource) return false; if (wanted.resource === "repo") { const asked = grant.params.getAll("action"); const held = asked.length === 0 ? ALL_ACTIONS : asked.filter(isRepoAction); return ( listOf(grant, "collection").some((granted) => nameMatches(granted, wanted.collection) ) && wanted.actions.every((action) => held.includes(action)) ); } const auds = grant.params.getAll("aud"); return ( auds.some((aud) => aud === ANY || aud === wanted.aud) && listOf(grant, "lxm").some((granted) => nameMatches(granted, wanted.lxm)) ); }; export const missingPermissions = ( granted: string, wanted: readonly Permission[] ): readonly Permission[] => { const scopes = granted.split(/\s+/).filter((scope) => scope !== ""); const unreadable = scopes.some( (scope) => scope === "transition:generic" || scope.startsWith("include:") ); if (unreadable) return []; const grants = scopes.map(grantFrom); return wanted.filter((permission) => !grants.some((grant) => covers(grant, permission))); };