@doist/comms-sdk 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/dist/cjs/authentication.js +55 -10
- package/dist/esm/authentication.js +54 -10
- package/dist/types/authentication.d.ts +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,24 @@ const api = new CommsApi(tokenResponse.accessToken)
|
|
|
103
103
|
const user = await api.users.getSessionUser()
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
Access tokens expire (see `tokenResponse.expiresIn`). When they do, use the
|
|
107
|
+
`refreshToken` from the original exchange to obtain a new one without sending
|
|
108
|
+
the user back through the authorization flow:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { refreshAuthToken } from '@doist/comms-sdk'
|
|
112
|
+
|
|
113
|
+
const refreshed = await refreshAuthToken({
|
|
114
|
+
clientId: 'your-client-id',
|
|
115
|
+
clientSecret: 'your-client-secret',
|
|
116
|
+
refreshToken: tokenResponse.refreshToken,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// The server may rotate the refresh token — persist refreshed.refreshToken
|
|
120
|
+
// when present, otherwise keep using the previous one.
|
|
121
|
+
const api = new CommsApi(refreshed.accessToken)
|
|
122
|
+
```
|
|
123
|
+
|
|
106
124
|
### Short-lived processes (CLIs, scripts)
|
|
107
125
|
|
|
108
126
|
On Node, the SDK keeps a connection pool alive across requests so HTTP/2
|
|
@@ -122,7 +140,7 @@ try {
|
|
|
122
140
|
|
|
123
141
|
`api.close()` drains the process-global pool, so it also covers code
|
|
124
142
|
paths that only use the standalone OAuth helpers (`getAuthToken`,
|
|
125
|
-
`revokeAuthToken`, `registerClient`). Those flows can also import
|
|
143
|
+
`refreshAuthToken`, `revokeAuthToken`, `registerClient`). Those flows can also import
|
|
126
144
|
`closeDefaultDispatcher` directly.
|
|
127
145
|
|
|
128
146
|
## Development
|
|
@@ -4,11 +4,25 @@ exports.TOKEN_ENDPOINT_AUTH_METHODS = exports.COMMS_SCOPES = void 0;
|
|
|
4
4
|
exports.getAuthStateParameter = getAuthStateParameter;
|
|
5
5
|
exports.getAuthorizationUrl = getAuthorizationUrl;
|
|
6
6
|
exports.getAuthToken = getAuthToken;
|
|
7
|
+
exports.refreshAuthToken = refreshAuthToken;
|
|
7
8
|
exports.revokeAuthToken = revokeAuthToken;
|
|
8
9
|
exports.registerClient = registerClient;
|
|
9
10
|
const uuid_1 = require("uuid");
|
|
10
11
|
const http_client_1 = require("./transport/http-client");
|
|
11
12
|
const errors_1 = require("./types/errors");
|
|
13
|
+
/**
|
|
14
|
+
* Default origin of the Comms authorization server.
|
|
15
|
+
*
|
|
16
|
+
* The authorization server for Comms is Todoist (`todoist.com`), not the
|
|
17
|
+
* resource server (`comms.todoist.com`). Tokens are issued by Todoist and
|
|
18
|
+
* accepted by the Comms API. Paths match Todoist's published
|
|
19
|
+
* `/.well-known/oauth-authorization-server` metadata.
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_AUTH_BASE_URL = 'https://todoist.com';
|
|
22
|
+
function getAuthBaseUrl(baseUrl) {
|
|
23
|
+
const base = baseUrl ?? DEFAULT_AUTH_BASE_URL;
|
|
24
|
+
return base.endsWith('/') ? base.slice(0, -1) : base;
|
|
25
|
+
}
|
|
12
26
|
/**
|
|
13
27
|
* OAuth scopes for the Comms API.
|
|
14
28
|
*
|
|
@@ -113,7 +127,7 @@ function getAuthorizationUrl(clientId, scopes, state, redirectUri, baseUrl) {
|
|
|
113
127
|
if (!scopes?.length) {
|
|
114
128
|
throw new Error('At least one scope value is required.');
|
|
115
129
|
}
|
|
116
|
-
const authBaseUrl =
|
|
130
|
+
const authBaseUrl = `${getAuthBaseUrl(baseUrl)}/oauth`;
|
|
117
131
|
const scope = scopes.join(' ');
|
|
118
132
|
const params = new URLSearchParams({
|
|
119
133
|
client_id: clientId,
|
|
@@ -127,9 +141,7 @@ function getAuthorizationUrl(clientId, scopes, state, redirectUri, baseUrl) {
|
|
|
127
141
|
return `${authBaseUrl}/authorize?${params.toString()}`;
|
|
128
142
|
}
|
|
129
143
|
async function getAuthToken(args, options) {
|
|
130
|
-
const tokenUrl = options?.baseUrl
|
|
131
|
-
? `${options.baseUrl}/oauth/token`
|
|
132
|
-
: 'https://comms.todoist.com/oauth/token';
|
|
144
|
+
const tokenUrl = `${getAuthBaseUrl(options?.baseUrl)}/oauth/access_token`;
|
|
133
145
|
const payload = {
|
|
134
146
|
clientId: args.clientId,
|
|
135
147
|
clientSecret: args.clientSecret,
|
|
@@ -149,10 +161,45 @@ async function getAuthToken(args, options) {
|
|
|
149
161
|
}
|
|
150
162
|
return response.data;
|
|
151
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Exchanges a refresh token for a new access token using the OAuth2
|
|
166
|
+
* `refresh_token` grant.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```typescript
|
|
170
|
+
* const { accessToken, refreshToken } = await refreshAuthToken({
|
|
171
|
+
* clientId: 'client-id',
|
|
172
|
+
* clientSecret: 'client-secret',
|
|
173
|
+
* refreshToken: storedRefreshToken,
|
|
174
|
+
* })
|
|
175
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* @returns The refreshed token. The response may include a new `refreshToken`
|
|
178
|
+
* (rotated by the server); persist it in place of the previous one when present.
|
|
179
|
+
* @throws {@link CommsRequestError} If the refresh fails
|
|
180
|
+
*/
|
|
181
|
+
async function refreshAuthToken(args, options) {
|
|
182
|
+
const tokenUrl = `${getAuthBaseUrl(options?.baseUrl)}/oauth/access_token`;
|
|
183
|
+
const payload = {
|
|
184
|
+
clientId: args.clientId,
|
|
185
|
+
clientSecret: args.clientSecret,
|
|
186
|
+
refreshToken: args.refreshToken,
|
|
187
|
+
grantType: 'refresh_token',
|
|
188
|
+
};
|
|
189
|
+
const response = await (0, http_client_1.request)({
|
|
190
|
+
httpMethod: 'POST',
|
|
191
|
+
baseUri: tokenUrl,
|
|
192
|
+
relativePath: '',
|
|
193
|
+
payload,
|
|
194
|
+
customFetch: options?.customFetch,
|
|
195
|
+
});
|
|
196
|
+
if (!(0, http_client_1.isSuccess)(response) || !response.data?.accessToken) {
|
|
197
|
+
throw new errors_1.CommsRequestError('Authentication token refresh failed.', response.status, response.data);
|
|
198
|
+
}
|
|
199
|
+
return response.data;
|
|
200
|
+
}
|
|
152
201
|
async function revokeAuthToken(args, options) {
|
|
153
|
-
const revokeUrl = options?.baseUrl
|
|
154
|
-
? `${options.baseUrl}/oauth/revoke`
|
|
155
|
-
: 'https://comms.todoist.com/oauth/revoke';
|
|
202
|
+
const revokeUrl = `${getAuthBaseUrl(options?.baseUrl)}/api/v1/revoke`;
|
|
156
203
|
const response = await (0, http_client_1.request)({
|
|
157
204
|
httpMethod: 'POST',
|
|
158
205
|
baseUri: revokeUrl,
|
|
@@ -184,9 +231,7 @@ async function revokeAuthToken(args, options) {
|
|
|
184
231
|
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591 RFC 7591}
|
|
185
232
|
*/
|
|
186
233
|
async function registerClient(args, options) {
|
|
187
|
-
const registerUrl = options?.baseUrl
|
|
188
|
-
? `${options.baseUrl}/oauth/register`
|
|
189
|
-
: 'https://comms.todoist.com/oauth/register';
|
|
234
|
+
const registerUrl = `${getAuthBaseUrl(options?.baseUrl)}/oauth/register`;
|
|
190
235
|
const response = await (0, http_client_1.request)({
|
|
191
236
|
httpMethod: 'POST',
|
|
192
237
|
baseUri: registerUrl,
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { v4 as uuid } from 'uuid';
|
|
2
2
|
import { isSuccess, request } from './transport/http-client.js';
|
|
3
3
|
import { CommsRequestError } from './types/errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* Default origin of the Comms authorization server.
|
|
6
|
+
*
|
|
7
|
+
* The authorization server for Comms is Todoist (`todoist.com`), not the
|
|
8
|
+
* resource server (`comms.todoist.com`). Tokens are issued by Todoist and
|
|
9
|
+
* accepted by the Comms API. Paths match Todoist's published
|
|
10
|
+
* `/.well-known/oauth-authorization-server` metadata.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_AUTH_BASE_URL = 'https://todoist.com';
|
|
13
|
+
function getAuthBaseUrl(baseUrl) {
|
|
14
|
+
const base = baseUrl ?? DEFAULT_AUTH_BASE_URL;
|
|
15
|
+
return base.endsWith('/') ? base.slice(0, -1) : base;
|
|
16
|
+
}
|
|
4
17
|
/**
|
|
5
18
|
* OAuth scopes for the Comms API.
|
|
6
19
|
*
|
|
@@ -105,7 +118,7 @@ export function getAuthorizationUrl(clientId, scopes, state, redirectUri, baseUr
|
|
|
105
118
|
if (!scopes?.length) {
|
|
106
119
|
throw new Error('At least one scope value is required.');
|
|
107
120
|
}
|
|
108
|
-
const authBaseUrl =
|
|
121
|
+
const authBaseUrl = `${getAuthBaseUrl(baseUrl)}/oauth`;
|
|
109
122
|
const scope = scopes.join(' ');
|
|
110
123
|
const params = new URLSearchParams({
|
|
111
124
|
client_id: clientId,
|
|
@@ -119,9 +132,7 @@ export function getAuthorizationUrl(clientId, scopes, state, redirectUri, baseUr
|
|
|
119
132
|
return `${authBaseUrl}/authorize?${params.toString()}`;
|
|
120
133
|
}
|
|
121
134
|
export async function getAuthToken(args, options) {
|
|
122
|
-
const tokenUrl = options?.baseUrl
|
|
123
|
-
? `${options.baseUrl}/oauth/token`
|
|
124
|
-
: 'https://comms.todoist.com/oauth/token';
|
|
135
|
+
const tokenUrl = `${getAuthBaseUrl(options?.baseUrl)}/oauth/access_token`;
|
|
125
136
|
const payload = {
|
|
126
137
|
clientId: args.clientId,
|
|
127
138
|
clientSecret: args.clientSecret,
|
|
@@ -141,10 +152,45 @@ export async function getAuthToken(args, options) {
|
|
|
141
152
|
}
|
|
142
153
|
return response.data;
|
|
143
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Exchanges a refresh token for a new access token using the OAuth2
|
|
157
|
+
* `refresh_token` grant.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* const { accessToken, refreshToken } = await refreshAuthToken({
|
|
162
|
+
* clientId: 'client-id',
|
|
163
|
+
* clientSecret: 'client-secret',
|
|
164
|
+
* refreshToken: storedRefreshToken,
|
|
165
|
+
* })
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* @returns The refreshed token. The response may include a new `refreshToken`
|
|
169
|
+
* (rotated by the server); persist it in place of the previous one when present.
|
|
170
|
+
* @throws {@link CommsRequestError} If the refresh fails
|
|
171
|
+
*/
|
|
172
|
+
export async function refreshAuthToken(args, options) {
|
|
173
|
+
const tokenUrl = `${getAuthBaseUrl(options?.baseUrl)}/oauth/access_token`;
|
|
174
|
+
const payload = {
|
|
175
|
+
clientId: args.clientId,
|
|
176
|
+
clientSecret: args.clientSecret,
|
|
177
|
+
refreshToken: args.refreshToken,
|
|
178
|
+
grantType: 'refresh_token',
|
|
179
|
+
};
|
|
180
|
+
const response = await request({
|
|
181
|
+
httpMethod: 'POST',
|
|
182
|
+
baseUri: tokenUrl,
|
|
183
|
+
relativePath: '',
|
|
184
|
+
payload,
|
|
185
|
+
customFetch: options?.customFetch,
|
|
186
|
+
});
|
|
187
|
+
if (!isSuccess(response) || !response.data?.accessToken) {
|
|
188
|
+
throw new CommsRequestError('Authentication token refresh failed.', response.status, response.data);
|
|
189
|
+
}
|
|
190
|
+
return response.data;
|
|
191
|
+
}
|
|
144
192
|
export async function revokeAuthToken(args, options) {
|
|
145
|
-
const revokeUrl = options?.baseUrl
|
|
146
|
-
? `${options.baseUrl}/oauth/revoke`
|
|
147
|
-
: 'https://comms.todoist.com/oauth/revoke';
|
|
193
|
+
const revokeUrl = `${getAuthBaseUrl(options?.baseUrl)}/api/v1/revoke`;
|
|
148
194
|
const response = await request({
|
|
149
195
|
httpMethod: 'POST',
|
|
150
196
|
baseUri: revokeUrl,
|
|
@@ -176,9 +222,7 @@ export async function revokeAuthToken(args, options) {
|
|
|
176
222
|
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591 RFC 7591}
|
|
177
223
|
*/
|
|
178
224
|
export async function registerClient(args, options) {
|
|
179
|
-
const registerUrl = options?.baseUrl
|
|
180
|
-
? `${options.baseUrl}/oauth/register`
|
|
181
|
-
: 'https://comms.todoist.com/oauth/register';
|
|
225
|
+
const registerUrl = `${getAuthBaseUrl(options?.baseUrl)}/oauth/register`;
|
|
182
226
|
const response = await request({
|
|
183
227
|
httpMethod: 'POST',
|
|
184
228
|
baseUri: registerUrl,
|
|
@@ -78,6 +78,11 @@ export type AuthTokenResponse = {
|
|
|
78
78
|
expiresIn?: number;
|
|
79
79
|
scope?: string;
|
|
80
80
|
};
|
|
81
|
+
export type RefreshAuthTokenRequestArgs = {
|
|
82
|
+
clientId: string;
|
|
83
|
+
clientSecret: string;
|
|
84
|
+
refreshToken: string;
|
|
85
|
+
};
|
|
81
86
|
export type RevokeAuthTokenRequestArgs = {
|
|
82
87
|
clientId: string;
|
|
83
88
|
clientSecret: string;
|
|
@@ -138,6 +143,24 @@ export declare function getAuthStateParameter(): string;
|
|
|
138
143
|
*/
|
|
139
144
|
export declare function getAuthorizationUrl(clientId: string, scopes: CommsScope[], state: string, redirectUri?: string, baseUrl?: string): string;
|
|
140
145
|
export declare function getAuthToken(args: AuthTokenRequestArgs, options?: AuthOptions): Promise<AuthTokenResponse>;
|
|
146
|
+
/**
|
|
147
|
+
* Exchanges a refresh token for a new access token using the OAuth2
|
|
148
|
+
* `refresh_token` grant.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* const { accessToken, refreshToken } = await refreshAuthToken({
|
|
153
|
+
* clientId: 'client-id',
|
|
154
|
+
* clientSecret: 'client-secret',
|
|
155
|
+
* refreshToken: storedRefreshToken,
|
|
156
|
+
* })
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* @returns The refreshed token. The response may include a new `refreshToken`
|
|
160
|
+
* (rotated by the server); persist it in place of the previous one when present.
|
|
161
|
+
* @throws {@link CommsRequestError} If the refresh fails
|
|
162
|
+
*/
|
|
163
|
+
export declare function refreshAuthToken(args: RefreshAuthTokenRequestArgs, options?: AuthOptions): Promise<AuthTokenResponse>;
|
|
141
164
|
export declare function revokeAuthToken(args: RevokeAuthTokenRequestArgs, options?: AuthOptions): Promise<boolean>;
|
|
142
165
|
/**
|
|
143
166
|
* Registers a new OAuth client via Dynamic Client Registration (RFC 7591).
|