Skip to content

Commit 14172a5

Browse files
authored
Updates for Beta: Adding scopes and removing unsupported methods (#99)
1 parent 2a57c32 commit 14172a5

File tree

5 files changed

+192
-12
lines changed

5 files changed

+192
-12
lines changed

.github/workflows/semgrep.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ on:
33
pull_request: {}
44
push:
55
branches:
6-
- main
7-
- master
6+
- main
7+
- master
88
paths:
9-
- .github/workflows/semgrep.yml
9+
- .github/workflows/semgrep.yml
1010
schedule:
11-
# random HH:MM to avoid a load spike on GitHub Actions at 00:00
12-
- cron: 34 11 * * *
11+
# random HH:MM to avoid a load spike on GitHub Actions at 00:00
12+
- cron: 34 11 * * *
1313
name: Semgrep
1414
jobs:
1515
semgrep:
@@ -20,5 +20,5 @@ jobs:
2020
container:
2121
image: returntocorp/semgrep
2222
steps:
23-
- uses: actions/checkout@v3
24-
- run: semgrep ci
23+
- uses: actions/checkout@v3
24+
- run: semgrep ci

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,41 @@ const webflow = new Webflow({
3838
});
3939
```
4040

41+
## Transitioning to API v2
42+
43+
We're actively working on a new version of the SDK that will fully support API v2. In the meantime, to make use of API v2 with our SDK, there are some important changes you need to be aware of:
44+
45+
### Setting Up For API v2
46+
47+
When initializing your client, it's crucial to set the `beta` flag to true in the client options. This ensures you're targeting the API v2 endpoints.
48+
49+
```javascript
50+
const webflow = new Webflow({ beta: true, ...otherOptions });
51+
```
52+
53+
Please note, when the beta flag is set, several built-in methods will not be available. These methods include, but are not limited to, info, authenticatedUser, sites, site, etc. Attempting to use these will throw an error.
54+
55+
### Calling API v2 Endpoints
56+
57+
To interact with API v2, you'll need to move away from using built-in methods, and instead use the provided HTTP methods directly.
58+
59+
For instance, where you previously used `sites()`:
60+
61+
```javascript
62+
// get the first site
63+
const [site] = await webflow.sites();
64+
```
65+
66+
For API v2, you will need to use direct HTTP methods:
67+
68+
```javascript
69+
// get the first site
70+
const sites = await webflow.get("/sites");
71+
const site = sites[0];
72+
```
73+
74+
We understand that this is a shift in how you interact with the SDK, but rest assured, our upcoming SDK version will streamline this process and offer a more integrated experience with API v2.
75+
4176
## Basic Usage
4277

4378
### Chaining Calls
@@ -131,6 +166,20 @@ const url = webflow.authorizeUrl({
131166
res.redirect(url);
132167
```
133168

169+
### Using the scopes Parameter with v2 API
170+
171+
The v2 API introduces the concept of 'scopes', providing more control over app permissions. Instead of using the scope parameter as a single string, you can define multiple permissions using the scopes array:
172+
173+
```javascript
174+
const url = webflow.authorizeUrl({
175+
client_id: "[CLIENT ID]",
176+
redirect_uri: "https://my.server.com/oauth/callback",
177+
scopes: ["read:sites", "write:items", "read:users"],
178+
});
179+
```
180+
181+
For more information and a detailed list of available scopes, refer to our Scopes Guide.
182+
134183
### Access Token
135184

136185
Once a user has authorized their Webflow resource(s), Webflow will redirect back to your server with a `code`. Use this to get an access token.

src/api/oauth.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { AxiosInstance } from "axios";
2-
import { requireArgs } from "../core";
2+
import { requireArgs, SupportedScope } from "../core";
33

44
/**************************************************************
55
* Interfaces
66
**************************************************************/
77
export interface IAuthorizeUrlParams {
88
state?: string;
99
scope?: string;
10+
scopes?: SupportedScope[];
1011
client_id: string;
1112
redirect_uri?: string;
1213
response_type?: string;
@@ -52,15 +53,20 @@ export class OAuth {
5253
* @returns The URL to authorize a user
5354
*/
5455
static authorizeUrl(
55-
{ response_type = "code", redirect_uri, client_id, state, scope }: IAuthorizeUrlParams,
56+
{ response_type = "code", redirect_uri, client_id, state, scope, scopes }: IAuthorizeUrlParams,
5657
client: AxiosInstance,
5758
) {
5859
requireArgs({ client_id });
5960

61+
if (scope && scopes) {
62+
throw new Error("Please provide either 'scope' or 'scopes', but not both.");
63+
}
64+
6065
const params = { response_type, client_id };
6166
if (redirect_uri) params["redirect_uri"] = redirect_uri;
6267
if (state) params["state"] = state;
6368
if (scope) params["scope"] = scope;
69+
if (scopes && scopes.length > 0) params["scope"] = scopes.join("+");
6470

6571
const url = "/oauth/authorize";
6672
const baseURL = client.defaults.baseURL.replace("api.", "");

src/core/webflow.ts

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,51 @@ export interface Options {
2222
token?: string;
2323
version?: string;
2424
headers?: Record<string, string>;
25+
beta?: boolean;
2526
}
2627

28+
type MethodNames =
29+
| "info"
30+
| "authenticatedUser"
31+
| "sites"
32+
| "site"
33+
| "publishSite"
34+
| "domains"
35+
| "collections"
36+
| "collection"
37+
| "items"
38+
| "item"
39+
| "createItem"
40+
| "updateItem"
41+
| "patchItem"
42+
| "removeItem"
43+
| "deleteItems"
44+
| "publishItems";
45+
46+
export const SCOPES_ARRAY = [
47+
"assets:read",
48+
"assets:write",
49+
"authorized_user:read",
50+
"cms:read",
51+
"cms:write",
52+
"custom_code:read",
53+
"custom_code:write",
54+
"forms:read",
55+
"forms:write",
56+
"pages:read",
57+
"pages:write",
58+
"sites:read",
59+
"sites:write",
60+
"users:read",
61+
"users:write",
62+
"ecommerce:read",
63+
"ecommerce:write",
64+
] as const;
65+
66+
export type SupportedScope = typeof SCOPES_ARRAY[number];
67+
68+
type ParamValueType = string | number | boolean | null | undefined;
69+
2770
/**************************************************************
2871
* Class
2972
**************************************************************/
@@ -32,6 +75,43 @@ export class Webflow {
3275
constructor(public options: Options = {}) {
3376
this.client = axios.create(this.config);
3477
this.client.interceptors.response.use(ErrorInterceptor);
78+
79+
if (this.options.beta) {
80+
this.removeNonBetaMethods();
81+
}
82+
}
83+
84+
private removeNonBetaMethods() {
85+
const methodsToRemove: MethodNames[] = [
86+
"info",
87+
"authenticatedUser",
88+
"sites",
89+
"site",
90+
"publishSite",
91+
"domains",
92+
"collections",
93+
"collection",
94+
"items",
95+
"item",
96+
"createItem",
97+
"updateItem",
98+
"patchItem",
99+
"removeItem",
100+
"deleteItems",
101+
"publishItems",
102+
];
103+
104+
methodsToRemove.forEach((method) => {
105+
Object.defineProperty(this, method, {
106+
value: function (): never {
107+
throw new Error(
108+
`The method '${method}()' is not available in beta mode. Please disable the beta option to use this method.`,
109+
);
110+
},
111+
enumerable: false,
112+
configurable: true,
113+
});
114+
});
35115
}
36116

37117
// Set the Authentication token
@@ -46,10 +126,11 @@ export class Webflow {
46126

47127
// The Axios configuration
48128
get config() {
49-
const { host = DEFAULT_HOST, token, version, headers } = this.options;
129+
const { host = DEFAULT_HOST, token, version, headers, beta = false } = this.options;
130+
const effectiveHost = beta ? "webflow.com/beta" : host;
50131

51132
const config: AxiosRequestConfig = {
52-
baseURL: `https://api.${host}/`,
133+
baseURL: `https://api.${effectiveHost}/`,
53134
headers: {
54135
"Content-Type": "application/json",
55136
"User-Agent": USER_AGENT,
@@ -63,6 +144,32 @@ export class Webflow {
63144
// Add the Authorization header if a token is set
64145
if (token) config.headers.Authorization = `Bearer ${token}`;
65146

147+
config.paramsSerializer = {
148+
serialize: (params: Record<string, ParamValueType>): string => {
149+
if (typeof params !== "object" || params === null) {
150+
return "";
151+
}
152+
153+
const parts: string[] = [];
154+
155+
for (const key in params) {
156+
const value = params[key];
157+
if (value === undefined) continue;
158+
159+
const safeValue =
160+
typeof value === "string" || typeof value === "number" ? value : String(value);
161+
162+
if (key === "scope") {
163+
parts.push(`${key}=${safeValue}`);
164+
} else {
165+
parts.push(`${key}=${encodeURIComponent(safeValue)}`);
166+
}
167+
}
168+
169+
return parts.join("&");
170+
},
171+
};
172+
66173
return config;
67174
}
68175

tests/core/webflow.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import axios from "axios";
22
import MockAdapter from "axios-mock-adapter";
3-
import { Webflow, RequestError } from "../../src/core";
3+
import { Webflow, RequestError, SupportedScope } from "../../src/core";
44
import {
55
MetaFixture,
66
SiteFixture,
@@ -108,6 +108,24 @@ describe("Webflow", () => {
108108
expect(url).toBe(`https://${options.host}/oauth/authorize?${query.toString()}`);
109109
});
110110

111+
it("should generate an authorization url with valid scopes", () => {
112+
const { parameters } = OAuthFixture.authorize;
113+
const { client_id, state, response_type } = parameters;
114+
const scopes: SupportedScope[] = ["assets:read", "cms:write"];
115+
116+
const url = webflow.authorizeUrl({ client_id, state, scopes });
117+
const queryParts = [
118+
`response_type=${response_type}`,
119+
`client_id=${client_id}`,
120+
`state=${state}`,
121+
`scope=${scopes.join("+")}`,
122+
];
123+
const query = queryParts.join("&");
124+
125+
expect(url).toBeDefined();
126+
expect(url).toBe(`https://${options.host}/oauth/authorize?${query}`);
127+
});
128+
111129
it("should generate an access token", async () => {
112130
const { parameters, response, path } = OAuthFixture.access_token;
113131

0 commit comments

Comments
 (0)