@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 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
 
@@ -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
- return (oldConfig.auth.type !== newConfig.auth.type ||
143
- oldConfig.auth.accessToken !== newConfig.auth.accessToken ||
144
- oldConfig.auth.refreshToken !== newConfig.auth.refreshToken ||
145
- oldConfig.auth.personalAccessToken !== newConfig.auth.personalAccessToken ||
146
- oldConfig.baseURL !== newConfig.baseURL ||
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
  }
@@ -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
- // Convert v2.0 config to v1.x format for auth functions
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 (0, auth_1.attemptTokenRefresh)(clientState, () => this.makeRequest(options, requestId));
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.personalAccessToken) {
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.accessToken) {
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 = {
@@ -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.0.0",
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",