Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support oauth token endpoint, add wiki verification support, fix issue #420 #438

Merged
merged 1 commit into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
55 changes: 53 additions & 2 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ import {
ListCommentsParameters,
ListCommentsResponse,
listComments,
OauthTokenResponse,
OauthTokenParameters,
oauthToken,
} from "./api-endpoints"
import nodeFetch from "node-fetch"
import {
Expand All @@ -98,7 +101,17 @@ export interface RequestParameters {
method: Method
query?: QueryParams
body?: Record<string, unknown>
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 {
Expand Down Expand Up @@ -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<string, string>
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<string, string> = {
...this.authAsHeaders(auth),
...authorizationHeader,
"Notion-Version": this.#notionVersion,
"user-agent": this.#userAgent,
}
Expand Down Expand Up @@ -525,6 +553,29 @@ export default class Client {
})
}

public readonly oauth = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allows you to call the token endpoint.

const { Client } = require("@notionhq/client")

// Initializing a client
const notion = new Client({ ... })

const tokenResponse = await notion.oauth.token({ ... })

This is similar to how we nest all the block methods under a blocks object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm... we were not allowed to call the token endpoint before your change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not through the SDK 😓 See #73

/**
* Get token
*/
token: (
args: OauthTokenParameters & {
client_id: string
client_secret: string
}
): Promise<OauthTokenResponse> => {
return this.request<OauthTokenResponse>({
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.
*
Expand Down
132 changes: 129 additions & 3 deletions src/api-endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,57 @@ type FormulaPropertyResponse =
| NumberFormulaPropertyResponse
| BooleanFormulaPropertyResponse

type VerificationPropertyUnverifiedResponse = {
state: "unverified"
date: null
verified_by: null
}

type VerificationPropertyResponse = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it auto-generated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

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
Expand Down Expand Up @@ -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<RichTextItemResponse>; id: string }
| { type: "rich_text"; rich_text: Array<RichTextItemResponse>; id: string }
| {
Expand Down Expand Up @@ -4790,7 +4850,11 @@ export type ParagraphBlockObjectResponse = {

export type Heading1BlockObjectResponse = {
type: "heading_1"
heading_1: { rich_text: Array<RichTextItemResponse>; color: ApiColor }
heading_1: {
rich_text: Array<RichTextItemResponse>
color: ApiColor
is_toggleable: boolean
}
parent:
| { type: "database_id"; database_id: string }
| { type: "page_id"; page_id: string }
Expand All @@ -4808,7 +4872,11 @@ export type Heading1BlockObjectResponse = {

export type Heading2BlockObjectResponse = {
type: "heading_2"
heading_2: { rich_text: Array<RichTextItemResponse>; color: ApiColor }
heading_2: {
rich_text: Array<RichTextItemResponse>
color: ApiColor
is_toggleable: boolean
}
parent:
| { type: "database_id"; database_id: string }
| { type: "page_id"; page_id: string }
Expand All @@ -4826,7 +4894,11 @@ export type Heading2BlockObjectResponse = {

export type Heading3BlockObjectResponse = {
type: "heading_3"
heading_3: { rich_text: Array<RichTextItemResponse>; color: ApiColor }
heading_3: {
rich_text: Array<RichTextItemResponse>
color: ApiColor
is_toggleable: boolean
}
parent:
| { type: "database_id"; database_id: string }
| { type: "page_id"; page_id: string }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -5749,6 +5832,7 @@ export type PropertyItemObjectResponse =
| LastEditedTimePropertyItemObjectResponse
| FormulaPropertyItemObjectResponse
| UniqueIdPropertyItemObjectResponse
| VerificationPropertyItemObjectResponse
| TitlePropertyItemObjectResponse
| RichTextPropertyItemObjectResponse
| PeoplePropertyItemObjectResponse
Expand Down Expand Up @@ -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