From 0877d3478c680bf6b5860cfc21e7ab012084aad4 Mon Sep 17 00:00:00 2001 From: Ryan Hart Date: Fri, 11 Aug 2023 15:18:04 -0700 Subject: [PATCH] Support oauth token endpoint, add wiki verification support, fix bug with toggleable heading type (#438) --- .cspell.json | 3 +- src/Client.ts | 55 +++++++++++++++++- src/api-endpoints.ts | 132 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 184 insertions(+), 6 deletions(-) diff --git a/.cspell.json b/.cspell.json index 7337912d..558e81a1 100644 --- a/.cspell.json +++ b/.cspell.json @@ -12,7 +12,8 @@ "sendgrid", "blackmad", "octokit", - "printf" + "printf", + "is_toggleable" ], // flagWords - list of words to be always considered incorrect // This is useful for offensive words and common spelling errors. diff --git a/src/Client.ts b/src/Client.ts index 22698484..338807b9 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -73,6 +73,9 @@ import { ListCommentsParameters, ListCommentsResponse, listComments, + OauthTokenResponse, + OauthTokenParameters, + oauthToken, } from "./api-endpoints" import nodeFetch from "node-fetch" import { @@ -98,7 +101,17 @@ export interface RequestParameters { method: Method query?: QueryParams body?: Record - auth?: string + /** + * To authenticate using public API token, `auth` should be passed as a + * string. If you are trying to complete OAuth, then `auth` should be an object + * containing your integration's client ID and secret. + */ + auth?: + | string + | { + client_id: string + client_secret: string + } } export default class Client { @@ -165,8 +178,23 @@ export default class Client { } } + // Allow both client ID / client secret based auth as well as token based auth. + let authorizationHeader: Record + if (typeof auth === "object") { + // Client ID and secret based auth is **ONLY** supported when using the + // `/oauth/token` endpoint. If this is the case, handle formatting the + // authorization header as required by `Basic` auth. + const unencodedCredential = `${auth.client_id}:${auth.client_secret}` + const encodedCredential = + Buffer.from(unencodedCredential).toString("base64") + authorizationHeader = { authorization: `Basic ${encodedCredential}` } + } else { + // Otherwise format authorization header as `Bearer` token auth. + authorizationHeader = this.authAsHeaders(auth) + } + const headers: Record = { - ...this.authAsHeaders(auth), + ...authorizationHeader, "Notion-Version": this.#notionVersion, "user-agent": this.#userAgent, } @@ -525,6 +553,29 @@ export default class Client { }) } + public readonly oauth = { + /** + * Get token + */ + token: ( + args: OauthTokenParameters & { + client_id: string + client_secret: string + } + ): Promise => { + return this.request({ + path: oauthToken.path(), + method: oauthToken.method, + query: pick(args, oauthToken.queryParams), + body: pick(args, oauthToken.bodyParams), + auth: { + client_id: args.client_id, + client_secret: args.client_secret, + }, + }) + }, + } + /** * Emits a log message to the console. * diff --git a/src/api-endpoints.ts b/src/api-endpoints.ts index 2419c2da..ce14911f 100644 --- a/src/api-endpoints.ts +++ b/src/api-endpoints.ts @@ -688,6 +688,57 @@ type FormulaPropertyResponse = | NumberFormulaPropertyResponse | BooleanFormulaPropertyResponse +type VerificationPropertyUnverifiedResponse = { + state: "unverified" + date: null + verified_by: null +} + +type VerificationPropertyResponse = { + state: "verified" | "expired" + date: DateResponse | null + verified_by: + | { id: IdRequest } + | null + | { + person: { email?: string } + id: IdRequest + type?: "person" + name?: string | null + avatar_url?: string | null + object?: "user" + } + | null + | { + bot: + | EmptyObject + | { + owner: + | { + type: "user" + user: + | { + type: "person" + person: { email: string } + name: string | null + avatar_url: string | null + id: IdRequest + object: "user" + } + | PartialUserObjectResponse + } + | { type: "workspace"; workspace: true } + workspace_name: string | null + } + id: IdRequest + type?: "bot" + name?: string | null + avatar_url?: string | null + object?: "user" + } + | null +} + type AnnotationResponse = { bold: boolean italic: boolean @@ -4405,6 +4456,15 @@ export type PageObjectResponse = { unique_id: { prefix: string | null; number: number | null } id: string } + | { + type: "verification" + verification: + | VerificationPropertyUnverifiedResponse + | null + | VerificationPropertyResponse + | null + id: string + } | { type: "title"; title: Array; id: string } | { type: "rich_text"; rich_text: Array; id: string } | { @@ -4790,7 +4850,11 @@ export type ParagraphBlockObjectResponse = { export type Heading1BlockObjectResponse = { type: "heading_1" - heading_1: { rich_text: Array; color: ApiColor } + heading_1: { + rich_text: Array + color: ApiColor + is_toggleable: boolean + } parent: | { type: "database_id"; database_id: string } | { type: "page_id"; page_id: string } @@ -4808,7 +4872,11 @@ export type Heading1BlockObjectResponse = { export type Heading2BlockObjectResponse = { type: "heading_2" - heading_2: { rich_text: Array; color: ApiColor } + heading_2: { + rich_text: Array + color: ApiColor + is_toggleable: boolean + } parent: | { type: "database_id"; database_id: string } | { type: "page_id"; page_id: string } @@ -4826,7 +4894,11 @@ export type Heading2BlockObjectResponse = { export type Heading3BlockObjectResponse = { type: "heading_3" - heading_3: { rich_text: Array; color: ApiColor } + heading_3: { + rich_text: Array + color: ApiColor + is_toggleable: boolean + } parent: | { type: "database_id"; database_id: string } | { type: "page_id"; page_id: string } @@ -5688,6 +5760,17 @@ export type UniqueIdPropertyItemObjectResponse = { id: string } +export type VerificationPropertyItemObjectResponse = { + type: "verification" + verification: + | VerificationPropertyUnverifiedResponse + | null + | VerificationPropertyResponse + | null + object: "property_item" + id: string +} + export type TitlePropertyItemObjectResponse = { type: "title" title: RichTextItemResponse @@ -5749,6 +5832,7 @@ export type PropertyItemObjectResponse = | LastEditedTimePropertyItemObjectResponse | FormulaPropertyItemObjectResponse | UniqueIdPropertyItemObjectResponse + | VerificationPropertyItemObjectResponse | TitlePropertyItemObjectResponse | RichTextPropertyItemObjectResponse | PeoplePropertyItemObjectResponse @@ -10645,3 +10729,45 @@ export const listComments = { bodyParams: [], path: (): string => `comments`, } as const + +type OauthTokenBodyParameters = { + grant_type: string + code: string + redirect_uri?: string + external_account?: { key: string; name: string } +} + +export type OauthTokenParameters = OauthTokenBodyParameters + +export type OauthTokenResponse = { + access_token: string + token_type: "bearer" + bot_id: string + workspace_icon: string | null + workspace_name: string | null + workspace_id: string + owner: + | { + type: "user" + user: + | { + type: "person" + person: { email: string } + name: string | null + avatar_url: string | null + id: IdRequest + object: "user" + } + | PartialUserObjectResponse + } + | { type: "workspace"; workspace: true } + duplicated_template_id: string | null +} + +export const oauthToken = { + method: "post", + pathParams: [], + queryParams: [], + bodyParams: ["grant_type", "code", "redirect_uri", "external_account"], + path: (): string => `oauth/token`, +} as const