@rachelallyson/planning-center-people-ts 2.12.0 → 2.12.2

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,22 @@ 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.12.2] - 2026-01-20
9
+
10
+ ### ✨ **New Features**
11
+
12
+ - **Direct Personal Access Token Configuration**: Added support for passing `personalAccessTokenSecret` directly in config (alternative to environment variables)
13
+ - **Flexible Authentication**: Choose between environment variables or direct config based on your needs
14
+
15
+ ## [2.12.1] - 2026-01-20
16
+
17
+ ### ✨ **New Features**
18
+
19
+ - **Duplicate Prevention Integration Tests**: Added comprehensive integration tests for duplicate person prevention
20
+ - `duplicate-prevention-comprehensive.integration.test.ts` - Tests the bug scenario where PCO contact verification delays cause duplicates
21
+ - `duplicate-prevention-retry-exhaustion.integration.test.ts` - Tests retry logic when contact verification takes too long
22
+ - Enhanced test coverage for the retry logic that prevents duplicate person creation
23
+
8
24
  ## [2.12.0] - 2026-01-14
9
25
 
10
26
  ### ✨ **New Features**
@@ -151,6 +151,10 @@ class PcoClientManager {
151
151
  if (oldConfig.auth.type === 'personal_access_token' && newConfig.auth.type === 'personal_access_token') {
152
152
  return oldConfig.auth.personalAccessToken !== newConfig.auth.personalAccessToken;
153
153
  }
154
+ if (oldConfig.auth.type === 'basic' && newConfig.auth.type === 'basic') {
155
+ return (oldConfig.auth.appId !== newConfig.auth.appId ||
156
+ oldConfig.auth.appSecret !== newConfig.auth.appSecret);
157
+ }
154
158
  return (oldConfig.baseURL !== newConfig.baseURL ||
155
159
  oldConfig.timeout !== newConfig.timeout);
156
160
  }
package/dist/client.d.ts CHANGED
@@ -74,6 +74,5 @@ export declare class PcoClient implements EventEmitter {
74
74
  * Get all registered event types
75
75
  */
76
76
  eventTypes(): EventType[];
77
- private setupEventHandlers;
78
77
  private updateModules;
79
78
  }
package/dist/client.js CHANGED
@@ -36,7 +36,6 @@ class PcoClient {
36
36
  this.reports = new reports_1.ReportsModule(this.httpClient, this.paginationHelper, this.eventEmitter);
37
37
  this.batch = new planning_center_base_ts_1.BatchExecutor(this, this.eventEmitter);
38
38
  // Set up event handlers from config
39
- this.setupEventHandlers();
40
39
  }
41
40
  // EventEmitter implementation
42
41
  on(eventType, handler) {
@@ -95,24 +94,6 @@ class PcoClient {
95
94
  eventTypes() {
96
95
  return this.eventEmitter.eventTypes();
97
96
  }
98
- setupEventHandlers() {
99
- // Set up config event handlers
100
- if (this.config.events?.onError) {
101
- this.on('error', this.config.events.onError);
102
- }
103
- if (this.config.events?.onAuthFailure) {
104
- this.on('auth:failure', this.config.events.onAuthFailure);
105
- }
106
- if (this.config.events?.onRequestStart) {
107
- this.on('request:start', this.config.events.onRequestStart);
108
- }
109
- if (this.config.events?.onRequestComplete) {
110
- this.on('request:complete', this.config.events.onRequestComplete);
111
- }
112
- if (this.config.events?.onRateLimit) {
113
- this.on('rate:limit', this.config.events.onRateLimit);
114
- }
115
- }
116
97
  updateModules() {
117
98
  // Recreate modules with new HTTP client
118
99
  this.people = new people_1.PeopleModule(this.httpClient, this.paginationHelper, this.eventEmitter);
@@ -49,4 +49,8 @@ export declare class PcoHttpClient {
49
49
  * Get authentication header for external services (like file uploads)
50
50
  */
51
51
  getAuthHeader(): string;
52
+ /**
53
+ * Make HTTPS request using Node.js HTTPS module (fallback when fetch is unavailable)
54
+ */
55
+ private makeHttpsRequest;
52
56
  }
package/dist/core/http.js CHANGED
@@ -152,7 +152,7 @@ class PcoHttpClient {
152
152
  catch (refreshError) {
153
153
  console.warn('Token refresh failed:', refreshError);
154
154
  // Call the onRefreshFailure callback
155
- await this.config.auth.onRefreshFailure(refreshError);
155
+ await this.config.auth.onRefreshFailure?.(refreshError);
156
156
  throw refreshError;
157
157
  }
158
158
  }
@@ -195,9 +195,20 @@ class PcoHttpClient {
195
195
  }
196
196
  addAuthentication(headers) {
197
197
  if (this.config.auth.type === 'personal_access_token') {
198
- // Personal Access Tokens use HTTP Basic Auth format: app_id:secret
199
- // The personalAccessToken should be in the format "app_id:secret"
200
- headers.Authorization = `Basic ${Buffer.from(this.config.auth.personalAccessToken).toString('base64')}`;
198
+ // Personal Access Tokens use client_id:secret format with HTTP Basic Auth
199
+ // Get client ID from config (required)
200
+ const clientId = this.config.auth.personalAccessToken;
201
+ // Get client secret from config or environment (with config taking precedence)
202
+ const clientSecret = this.config.auth.personalAccessTokenSecret ||
203
+ process.env.PCO_PERSONAL_ACCESS_SECRET;
204
+ if (!clientId) {
205
+ throw new Error('personalAccessToken is required for personal access token authentication');
206
+ }
207
+ if (!clientSecret) {
208
+ throw new Error('personalAccessTokenSecret (in config) or PCO_PERSONAL_ACCESS_SECRET environment variable is required for personal access token authentication');
209
+ }
210
+ const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
211
+ headers.Authorization = `Basic ${credentials}`;
201
212
  }
202
213
  else if (this.config.auth.type === 'oauth') {
203
214
  headers.Authorization = `Bearer ${this.config.auth.accessToken}`;
@@ -243,11 +254,11 @@ class PcoHttpClient {
243
254
  // Prepare the request body for token refresh
244
255
  const body = new URLSearchParams({
245
256
  grant_type: 'refresh_token',
246
- refresh_token: this.config.auth.refreshToken,
257
+ refresh_token: this.config.auth.refreshToken || '',
247
258
  });
248
259
  // Add client credentials if available from the config or environment
249
- const clientId = this.config.auth.clientId || process.env.PCO_APP_ID;
250
- const clientSecret = this.config.auth.clientSecret || process.env.PCO_APP_SECRET;
260
+ const clientId = this.config.auth.appId || process.env.PCO_APP_ID;
261
+ const clientSecret = this.config.auth.appSecret || process.env.PCO_APP_SECRET;
251
262
  if (clientId && clientSecret) {
252
263
  body.append('client_id', clientId);
253
264
  body.append('client_secret', clientSecret);
@@ -268,10 +279,10 @@ class PcoHttpClient {
268
279
  // Update the config with new tokens
269
280
  this.config.auth.accessToken = tokens.access_token;
270
281
  this.config.auth.refreshToken = tokens.refresh_token || this.config.auth.refreshToken;
271
- // Call the onRefresh callback with the expected format
272
- await this.config.auth.onRefresh({
282
+ // Call the onRefresh callback
283
+ await this.config.auth.onRefresh?.({
273
284
  accessToken: tokens.access_token,
274
- refreshToken: tokens.refresh_token || this.config.auth.refreshToken,
285
+ refreshToken: tokens.refresh_token || this.config.auth.refreshToken
275
286
  });
276
287
  }
277
288
  updateRateLimitTracking(endpoint, headers) {
@@ -292,13 +303,58 @@ class PcoHttpClient {
292
303
  * Get authentication header for external services (like file uploads)
293
304
  */
294
305
  getAuthHeader() {
306
+ // The base package's PcoHttpClient handles authentication, so this method should delegate
307
+ // But for backward compatibility, we'll implement it
295
308
  if (this.config.auth.type === 'personal_access_token') {
296
- return `Basic ${Buffer.from(this.config.auth.personalAccessToken).toString('base64')}`;
309
+ const clientSecret = this.config.auth.personalAccessTokenSecret || process.env.PCO_PERSONAL_ACCESS_SECRET;
310
+ return `Basic ${Buffer.from(`${this.config.auth.personalAccessToken}:${clientSecret}`).toString('base64')}`;
297
311
  }
298
312
  else if (this.config.auth.type === 'oauth') {
299
313
  return `Bearer ${this.config.auth.accessToken}`;
300
314
  }
315
+ else if (this.config.auth.type === 'basic') {
316
+ return `Basic ${Buffer.from(`${this.config.auth.appId}:${this.config.auth.appSecret}`).toString('base64')}`;
317
+ }
301
318
  return '';
302
319
  }
320
+ /**
321
+ * Make HTTPS request using Node.js HTTPS module (fallback when fetch is unavailable)
322
+ */
323
+ async makeHttpsRequest(url, options) {
324
+ const https = require('https');
325
+ const urlObj = new URL(url);
326
+ const requestOptions = {
327
+ hostname: urlObj.hostname,
328
+ port: urlObj.port || 443,
329
+ path: urlObj.pathname + urlObj.search,
330
+ method: options.method || 'GET',
331
+ headers: options.headers,
332
+ };
333
+ return new Promise((resolve, reject) => {
334
+ const req = https.request(requestOptions, (res) => {
335
+ let data = '';
336
+ res.on('data', (chunk) => {
337
+ data += chunk;
338
+ });
339
+ res.on('end', () => {
340
+ // Create a response-like object
341
+ const response = {
342
+ ok: res.statusCode >= 200 && res.statusCode < 300,
343
+ status: res.statusCode,
344
+ statusText: res.statusMessage,
345
+ headers: res.headers,
346
+ text: () => Promise.resolve(data),
347
+ json: () => Promise.resolve(JSON.parse(data)),
348
+ };
349
+ resolve(response);
350
+ });
351
+ });
352
+ req.on('error', reject);
353
+ if (options.body) {
354
+ req.write(options.body);
355
+ }
356
+ req.end();
357
+ });
358
+ }
303
359
  }
304
360
  exports.PcoHttpClient = PcoHttpClient;
package/dist/core.d.ts CHANGED
@@ -3,8 +3,10 @@ import { PcoRateLimiter } from '@rachelallyson/planning-center-base-ts';
3
3
  import { Paginated, ResourceObject, Response as JsonApiResponse } from './types';
4
4
  import { type TokenRefreshCallback, type TokenRefreshFailureCallback } from './auth';
5
5
  export interface PcoClientConfig {
6
- /** Personal Access Token (for single-user apps) */
6
+ /** Personal Access Token Client ID (for single-user apps) */
7
7
  personalAccessToken?: string;
8
+ /** Personal Access Token Client Secret (alternative to PCO_PERSONAL_ACCESS_SECRET env var) */
9
+ personalAccessTokenSecret?: string;
8
10
  /** OAuth 2.0 Access Token (for multi-user apps) */
9
11
  accessToken?: string;
10
12
  /** OAuth 2.0 Refresh Token (for multi-user apps) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rachelallyson/planning-center-people-ts",
3
- "version": "2.12.0",
3
+ "version": "2.12.2",
4
4
  "description": "A strictly typed TypeScript client for Planning Center Online People API with comprehensive functionality and enhanced developer experience",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",