@rachelallyson/planning-center-people-ts 2.12.1 → 2.13.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,23 @@ 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.13.0] - 2026-01-20
9
+
10
+ ### ✨ **New Features**
11
+
12
+ - **🎯 Lenient Age Preference Filtering**: Added `agePreferenceLenient` option for flexible age-based filtering
13
+ - When `agePreferenceLenient: true`, age preferences only filter profiles that have birthdates
14
+ - Profiles without birthdates are included regardless of `agePreference` setting
15
+ - Prevents false negatives when searching for existing people with incomplete age data
16
+ - Backward compatible - default behavior remains strict filtering (only `agePreference: 'any'` includes profiles without birthdates)
17
+
18
+ ## [2.12.2] - 2026-01-20
19
+
20
+ ### ✨ **New Features**
21
+
22
+ - **Direct Personal Access Token Configuration**: Added support for passing `personalAccessTokenSecret` directly in config (alternative to environment variables)
23
+ - **Flexible Authentication**: Choose between environment variables or direct config based on your needs
24
+
8
25
  ## [2.12.1] - 2026-01-20
9
26
 
10
27
  ### ✨ **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/dist/helpers.d.ts CHANGED
@@ -35,6 +35,7 @@ export declare function matchesAgeCriteria(birthdate: string | undefined, criter
35
35
  minAge?: number;
36
36
  maxAge?: number;
37
37
  birthYear?: number;
38
+ agePreferenceLenient?: boolean;
38
39
  }): boolean;
39
40
  /**
40
41
  * Calculate birth year from age
package/dist/helpers.js CHANGED
@@ -103,8 +103,13 @@ function isChild(birthdate) {
103
103
  */
104
104
  function matchesAgeCriteria(birthdate, criteria) {
105
105
  const age = calculateAgeSafe(birthdate);
106
- // If no birthdate, only match if preference is 'any'
106
+ // If no birthdate, match based on lenient setting
107
107
  if (age === null) {
108
+ if (criteria.agePreferenceLenient) {
109
+ // Lenient mode: include profiles without birthdates regardless of agePreference
110
+ return true;
111
+ }
112
+ // Strict mode (default): only match if preference is 'any'
108
113
  return criteria.agePreference === 'any' || criteria.agePreference === undefined;
109
114
  }
110
115
  // Check age preference
@@ -377,7 +377,8 @@ class PersonMatcher {
377
377
  agePreference: options.agePreference,
378
378
  minAge: options.minAge,
379
379
  maxAge: options.maxAge,
380
- birthYear: options.birthYear
380
+ birthYear: options.birthYear,
381
+ agePreferenceLenient: options.agePreferenceLenient
381
382
  });
382
383
  });
383
384
  }
@@ -164,7 +164,8 @@ class MatchScorer {
164
164
  agePreference: options.agePreference,
165
165
  minAge: options.minAge,
166
166
  maxAge: options.maxAge,
167
- birthYear: options.birthYear
167
+ birthYear: options.birthYear,
168
+ agePreferenceLenient: options.agePreferenceLenient
168
169
  });
169
170
  if (!matches) {
170
171
  return 0; // No match
@@ -70,6 +70,12 @@ export interface PersonMatchOptions {
70
70
  addMissingContactInfo?: boolean;
71
71
  /** Age preference filter: 'adults' (18+), 'children' (<18), or 'any' */
72
72
  agePreference?: 'adults' | 'children' | 'any';
73
+ /**
74
+ * When true, age preference filters only apply to profiles with birthdates.
75
+ * Profiles without birthdates are included regardless of agePreference.
76
+ * When false (default), profiles without birthdates only match when agePreference is 'any'.
77
+ */
78
+ agePreferenceLenient?: boolean;
73
79
  /** Minimum age filter */
74
80
  minAge?: number;
75
81
  /** Maximum age filter */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rachelallyson/planning-center-people-ts",
3
- "version": "2.12.1",
3
+ "version": "2.13.0",
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",