@rachelallyson/planning-center-people-ts 2.0.0 → 2.1.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/CHANGELOG.md +65 -0
- package/dist/client-manager.js +14 -8
- package/dist/core/http.d.ts +1 -0
- package/dist/core/http.js +35 -12
- package/dist/index.d.ts +1 -1
- package/dist/testing/simple-factories.js +9 -0
- package/dist/types/client.d.ts +19 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,67 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.1.0] - 2025-01-17
|
|
9
|
+
|
|
10
|
+
### 🔒 **SECURITY RELEASE - Required Refresh Token Handling**
|
|
11
|
+
|
|
12
|
+
This release addresses a critical security issue where OAuth 2.0 clients could lose access when tokens expire without proper refresh handling.
|
|
13
|
+
|
|
14
|
+
### Breaking Changes
|
|
15
|
+
|
|
16
|
+
- **OAuth 2.0 Authentication**: `onRefresh` and `onRefreshFailure` callbacks are now **required** for OAuth configurations
|
|
17
|
+
- **Type Safety**: Enhanced type-safe authentication configuration prevents invalid configurations at compile time
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
|
|
21
|
+
- **CRITICAL**: OAuth 2.0 authentication now requires refresh token handling to prevent token loss
|
|
22
|
+
- **BREAKING**: Type-safe authentication configuration enforces required fields
|
|
23
|
+
- Enhanced token refresh implementation with proper error handling
|
|
24
|
+
- Improved authentication type safety with union types
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Fixed person matching to properly handle default fuzzy strategy
|
|
29
|
+
- Fixed mock client to support createWithContacts method
|
|
30
|
+
- Fixed event system tests to work with mock client
|
|
31
|
+
- Fixed phone number builder in mock response builder
|
|
32
|
+
|
|
33
|
+
### Migration from v2.0.0
|
|
34
|
+
|
|
35
|
+
**Before (v2.0.0):**
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
const client = new PcoClient({
|
|
39
|
+
auth: {
|
|
40
|
+
type: 'oauth',
|
|
41
|
+
accessToken: 'access-token',
|
|
42
|
+
refreshToken: 'refresh-token'
|
|
43
|
+
// Missing required callbacks - this will now cause TypeScript errors
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**After (v2.1.0):**
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const client = new PcoClient({
|
|
52
|
+
auth: {
|
|
53
|
+
type: 'oauth',
|
|
54
|
+
accessToken: 'access-token',
|
|
55
|
+
refreshToken: 'refresh-token',
|
|
56
|
+
// REQUIRED: Handle token refresh to prevent token loss
|
|
57
|
+
onRefresh: async (tokens) => {
|
|
58
|
+
await saveTokensToDatabase(userId, tokens);
|
|
59
|
+
},
|
|
60
|
+
// REQUIRED: Handle refresh failures
|
|
61
|
+
onRefreshFailure: async (error) => {
|
|
62
|
+
console.error('Token refresh failed:', error.message);
|
|
63
|
+
await clearUserTokens(userId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
8
69
|
## [2.0.0] - 2025-01-17
|
|
9
70
|
|
|
10
71
|
### 🚀 **MAJOR RELEASE - Complete API Redesign**
|
|
@@ -120,6 +181,10 @@ const person = await client.people.create({ first_name: 'John', last_name: 'Doe'
|
|
|
120
181
|
- **Error Handling**: Improved error handling and retry logic
|
|
121
182
|
- **Rate Limiting**: Fixed rate limiting edge cases
|
|
122
183
|
- **Authentication**: Resolved token refresh and persistence issues
|
|
184
|
+
- Fixed person matching to properly handle default fuzzy strategy
|
|
185
|
+
- Fixed mock client to support createWithContacts method
|
|
186
|
+
- Fixed event system tests to work with mock client
|
|
187
|
+
- Fixed phone number builder in mock response builder
|
|
123
188
|
|
|
124
189
|
## [1.1.0] - 2025-10-08
|
|
125
190
|
|
package/dist/client-manager.js
CHANGED
|
@@ -119,9 +119,9 @@ class PcoClientManager {
|
|
|
119
119
|
// Create a hash of the configuration
|
|
120
120
|
const configStr = JSON.stringify({
|
|
121
121
|
authType: config.auth.type,
|
|
122
|
-
hasAccessToken: !!config.auth.accessToken,
|
|
123
|
-
hasRefreshToken: !!config.auth.refreshToken,
|
|
124
|
-
hasPersonalAccessToken: !!config.auth.personalAccessToken,
|
|
122
|
+
hasAccessToken: config.auth.type === 'oauth' ? !!config.auth.accessToken : false,
|
|
123
|
+
hasRefreshToken: config.auth.type === 'oauth' ? !!config.auth.refreshToken : false,
|
|
124
|
+
hasPersonalAccessToken: config.auth.type === 'personal_access_token' ? !!config.auth.personalAccessToken : false,
|
|
125
125
|
baseURL: config.baseURL,
|
|
126
126
|
timeout: config.timeout,
|
|
127
127
|
});
|
|
@@ -139,11 +139,17 @@ class PcoClientManager {
|
|
|
139
139
|
*/
|
|
140
140
|
hasConfigChanged(oldConfig, newConfig) {
|
|
141
141
|
// Compare key configuration properties
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
oldConfig.
|
|
142
|
+
if (oldConfig.auth.type !== newConfig.auth.type) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
if (oldConfig.auth.type === 'oauth' && newConfig.auth.type === 'oauth') {
|
|
146
|
+
return (oldConfig.auth.accessToken !== newConfig.auth.accessToken ||
|
|
147
|
+
oldConfig.auth.refreshToken !== newConfig.auth.refreshToken);
|
|
148
|
+
}
|
|
149
|
+
if (oldConfig.auth.type === 'personal_access_token' && newConfig.auth.type === 'personal_access_token') {
|
|
150
|
+
return oldConfig.auth.personalAccessToken !== newConfig.auth.personalAccessToken;
|
|
151
|
+
}
|
|
152
|
+
return (oldConfig.baseURL !== newConfig.baseURL ||
|
|
147
153
|
oldConfig.timeout !== newConfig.timeout);
|
|
148
154
|
}
|
|
149
155
|
}
|
package/dist/core/http.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export declare class PcoHttpClient {
|
|
|
31
31
|
private addAuthentication;
|
|
32
32
|
private getResourceTypeFromEndpoint;
|
|
33
33
|
private extractHeaders;
|
|
34
|
+
private attemptTokenRefresh;
|
|
34
35
|
private updateRateLimitTracking;
|
|
35
36
|
getPerformanceMetrics(): Record<string, {
|
|
36
37
|
count: number;
|
package/dist/core/http.js
CHANGED
|
@@ -7,7 +7,6 @@ exports.PcoHttpClient = void 0;
|
|
|
7
7
|
const monitoring_1 = require("../monitoring");
|
|
8
8
|
const rate_limiter_1 = require("../rate-limiter");
|
|
9
9
|
const api_error_1 = require("../api-error");
|
|
10
|
-
const auth_1 = require("../auth");
|
|
11
10
|
class PcoHttpClient {
|
|
12
11
|
constructor(config, eventEmitter) {
|
|
13
12
|
this.config = config;
|
|
@@ -139,20 +138,15 @@ class PcoHttpClient {
|
|
|
139
138
|
// Handle other errors
|
|
140
139
|
if (!response.ok) {
|
|
141
140
|
// Handle 401 errors with token refresh if available
|
|
142
|
-
|
|
143
|
-
const v1Config = {
|
|
144
|
-
refreshToken: this.config.auth.refreshToken,
|
|
145
|
-
onTokenRefresh: this.config.auth.onRefresh,
|
|
146
|
-
onTokenRefreshFailure: this.config.auth.onRefreshFailure,
|
|
147
|
-
};
|
|
148
|
-
const clientState = { config: v1Config, rateLimiter: this.rateLimiter };
|
|
149
|
-
if (response.status === 401 && (0, auth_1.hasRefreshTokenCapability)(clientState)) {
|
|
141
|
+
if (response.status === 401 && this.config.auth.type === 'oauth') {
|
|
150
142
|
try {
|
|
151
|
-
await
|
|
143
|
+
await this.attemptTokenRefresh();
|
|
152
144
|
return this.makeRequest(options, requestId);
|
|
153
145
|
}
|
|
154
146
|
catch (refreshError) {
|
|
155
147
|
console.warn('Token refresh failed:', refreshError);
|
|
148
|
+
// Call the onRefreshFailure callback
|
|
149
|
+
await this.config.auth.onRefreshFailure(refreshError);
|
|
156
150
|
}
|
|
157
151
|
}
|
|
158
152
|
let errorData;
|
|
@@ -189,12 +183,12 @@ class PcoHttpClient {
|
|
|
189
183
|
}
|
|
190
184
|
}
|
|
191
185
|
addAuthentication(headers) {
|
|
192
|
-
if (this.config.auth.
|
|
186
|
+
if (this.config.auth.type === 'personal_access_token') {
|
|
193
187
|
// Personal Access Tokens use HTTP Basic Auth format: app_id:secret
|
|
194
188
|
// The personalAccessToken should be in the format "app_id:secret"
|
|
195
189
|
headers.Authorization = `Basic ${Buffer.from(this.config.auth.personalAccessToken).toString('base64')}`;
|
|
196
190
|
}
|
|
197
|
-
else if (this.config.auth.
|
|
191
|
+
else if (this.config.auth.type === 'oauth') {
|
|
198
192
|
headers.Authorization = `Bearer ${this.config.auth.accessToken}`;
|
|
199
193
|
}
|
|
200
194
|
}
|
|
@@ -224,6 +218,35 @@ class PcoHttpClient {
|
|
|
224
218
|
});
|
|
225
219
|
return headers;
|
|
226
220
|
}
|
|
221
|
+
async attemptTokenRefresh() {
|
|
222
|
+
if (this.config.auth.type !== 'oauth') {
|
|
223
|
+
throw new Error('Token refresh is only available for OAuth authentication');
|
|
224
|
+
}
|
|
225
|
+
const baseURL = this.config.baseURL || 'https://api.planningcenteronline.com/people/v2';
|
|
226
|
+
const tokenUrl = baseURL.replace('/people/v2', '/oauth/token');
|
|
227
|
+
const response = await fetch(tokenUrl, {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: {
|
|
230
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
231
|
+
},
|
|
232
|
+
body: new URLSearchParams({
|
|
233
|
+
grant_type: 'refresh_token',
|
|
234
|
+
refresh_token: this.config.auth.refreshToken,
|
|
235
|
+
}),
|
|
236
|
+
});
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`);
|
|
239
|
+
}
|
|
240
|
+
const tokens = await response.json();
|
|
241
|
+
// Update the config with new tokens
|
|
242
|
+
this.config.auth.accessToken = tokens.access_token;
|
|
243
|
+
this.config.auth.refreshToken = tokens.refresh_token;
|
|
244
|
+
// Call the onRefresh callback
|
|
245
|
+
await this.config.auth.onRefresh({
|
|
246
|
+
accessToken: tokens.access_token,
|
|
247
|
+
refreshToken: tokens.refresh_token,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
227
250
|
updateRateLimitTracking(endpoint, headers) {
|
|
228
251
|
const limit = headers['x-pco-api-request-rate-limit'];
|
|
229
252
|
const remaining = headers['x-pco-api-request-rate-count'];
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { PcoClient } from './client';
|
|
2
2
|
export { PcoClientManager } from './client-manager';
|
|
3
|
-
export type { PcoClientConfig } from './types/client';
|
|
3
|
+
export type { PcoClientConfig, PcoAuthConfig, PersonalAccessTokenAuth, OAuthAuth } from './types/client';
|
|
4
4
|
export type { PcoEvent, EventHandler, EventType } from './types/events';
|
|
5
5
|
export type { BatchOperation, BatchResult, BatchOptions, BatchSummary } from './types/batch';
|
|
6
6
|
export type { Paginated, Relationship, ResourceIdentifier, ResourceObject, } from './types';
|
|
@@ -181,6 +181,9 @@ function createTestClient(overrides = {}) {
|
|
|
181
181
|
auth: {
|
|
182
182
|
type: 'oauth',
|
|
183
183
|
accessToken: 'test-token',
|
|
184
|
+
refreshToken: 'test-refresh-token',
|
|
185
|
+
onRefresh: async () => { },
|
|
186
|
+
onRefreshFailure: async () => { },
|
|
184
187
|
},
|
|
185
188
|
};
|
|
186
189
|
const defaultMockConfig = {
|
|
@@ -239,6 +242,9 @@ function createErrorMockClient(errorType = 'network') {
|
|
|
239
242
|
auth: {
|
|
240
243
|
type: 'oauth',
|
|
241
244
|
accessToken: 'test-token',
|
|
245
|
+
refreshToken: 'test-refresh-token',
|
|
246
|
+
onRefresh: async () => { },
|
|
247
|
+
onRefreshFailure: async () => { },
|
|
242
248
|
},
|
|
243
249
|
};
|
|
244
250
|
const errorMockConfig = {
|
|
@@ -260,6 +266,9 @@ function createSlowMockClient(delayMs = 1000) {
|
|
|
260
266
|
auth: {
|
|
261
267
|
type: 'oauth',
|
|
262
268
|
accessToken: 'test-token',
|
|
269
|
+
refreshToken: 'test-refresh-token',
|
|
270
|
+
onRefresh: async () => { },
|
|
271
|
+
onRefreshFailure: async () => { },
|
|
263
272
|
},
|
|
264
273
|
};
|
|
265
274
|
const slowMockConfig = {
|
package/dist/types/client.d.ts
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* v2.0.0 Client Configuration Types
|
|
3
3
|
*/
|
|
4
|
+
/** Authentication configuration for Personal Access Token */
|
|
5
|
+
export interface PersonalAccessTokenAuth {
|
|
6
|
+
type: 'personal_access_token';
|
|
7
|
+
personalAccessToken: string;
|
|
8
|
+
}
|
|
9
|
+
/** Authentication configuration for OAuth 2.0 with required refresh handling */
|
|
10
|
+
export interface OAuthAuth {
|
|
11
|
+
type: 'oauth';
|
|
12
|
+
accessToken: string;
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
onRefresh: (tokens: {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
refreshToken: string;
|
|
17
|
+
}) => void | Promise<void>;
|
|
18
|
+
onRefreshFailure: (error: Error) => void | Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/** Union type for authentication configurations */
|
|
21
|
+
export type PcoAuthConfig = PersonalAccessTokenAuth | OAuthAuth;
|
|
4
22
|
export interface PcoClientConfig {
|
|
5
23
|
/** Authentication configuration */
|
|
6
|
-
auth:
|
|
7
|
-
type: 'oauth' | 'personal_access_token';
|
|
8
|
-
accessToken?: string;
|
|
9
|
-
refreshToken?: string;
|
|
10
|
-
personalAccessToken?: string;
|
|
11
|
-
onRefresh?: (tokens: {
|
|
12
|
-
accessToken: string;
|
|
13
|
-
refreshToken?: string;
|
|
14
|
-
}) => void | Promise<void>;
|
|
15
|
-
onRefreshFailure?: (error: Error) => void | Promise<void>;
|
|
16
|
-
};
|
|
24
|
+
auth: PcoAuthConfig;
|
|
17
25
|
/** Caching configuration */
|
|
18
26
|
caching?: {
|
|
19
27
|
fieldDefinitions?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rachelallyson/planning-center-people-ts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A strictly typed TypeScript client for Planning Center Online People API with smart matching, batch operations, and enhanced developer experience",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|