1import { createHash } from "node:crypto";
2import type { Level } from "../../lib/authorize.js";
3
4
25export const OAUTH_SCOPES = [
26 "pages:read",
27 "pages:write",
28 "databases:read",
29 "databases:write",
30 "files:read",
31 "files:write"
32] as const;
33export type OAuthScope = (typeof OAUTH_SCOPES)[number];
34
35
36export const OAUTH_SCOPE_RESOURCES = ["pages", "databases", "files"] as const;
37export type OAuthScopeResource = (typeof OAUTH_SCOPE_RESOURCES)[number];
38
39
40export function scopeResource(scope: string): OAuthScopeResource | null {
41 const i = scope.indexOf(":");
42 if (i === -1) return null;
43 const r = scope.slice(0, i);
44 return (OAUTH_SCOPE_RESOURCES as readonly string[]).includes(r) ? (r as OAuthScopeResource) : null;
45}
46
47
48export const OAUTH_SCOPE_DESCRIPTIONS: Record<OAuthScope, string> = {
49 "pages:read": "View pages",
50 "pages:write": "Create and edit pages",
51 "databases:read": "View databases and records",
52 "databases:write": "Create and edit databases and records",
53 "files:read": "View and download files",
54 "files:write": "Upload and manage files"
55};
56
57
69export function oauthScopeCap(scopes: string[]): Level {
70 if (scopes.some((s) => s.endsWith(":write"))) return "EDIT";
71 if (scopes.some((s) => s.endsWith(":read"))) return "VIEW";
72 return "NONE";
73}
74
75
84export function missingOAuthScope(
85 need: Level,
86 scopes: string[],
87 resource?: { pageId?: string; databaseId?: string; fileId?: string }
88): string {
89 const action: "read" | "write" = need === "VIEW" || need === "NONE" ? "read" : "write";
90 let r: OAuthScopeResource | null = null;
91 if (resource?.pageId) r = "pages";
92 else if (resource?.databaseId) r = "databases";
93 else if (resource?.fileId) r = "files";
94 else {
95 for (const s of scopes) {
96 const sr = scopeResource(s);
97 if (sr) {
98 r = sr;
99 break;
100 }
101 }
102 if (!r) r = "pages";
103 }
104 return `${r}:${action}`;
105}
106
107
108export function parseScopeList(scope?: string | null): string[] {
109 if (!scope) return [];
110 const seen = new Set<string>();
111 for (const part of scope.split(" ")) {
112 const s = part.trim();
113 if (s && !seen.has(s)) seen.add(s);
114 }
115 return [...seen];
116}
117
118
119export function serializeScopeList(scopes: string[]): string {
120 return OAUTH_SCOPES.filter((s) => scopes.includes(s as OAuthScope)).join(" ");
121}
122
123
124export function pkceChallenge(verifier: string): string {
125 return createHash("sha256")
126 .update(verifier, "ascii")
127 .digest("base64")
128 .replace(/\+/g, "-")
129 .replace(/\
130 .replace(/=+$/, "");
131}
132