@pushflodev/sdk 1.0.3 → 1.0.4

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 CHANGED
@@ -409,6 +409,67 @@ client.on('error', (error) => {
409
409
  | `ConnectionError` | WebSocket connection issues | Yes |
410
410
  | `AuthenticationError` | Invalid/missing API key | No |
411
411
  | `NetworkError` | HTTP request failures | Varies |
412
+ | `ValidationError` | Invalid input (e.g., channel slug) | No |
413
+
414
+ ## Channel Naming Rules
415
+
416
+ Channel slugs must follow these rules:
417
+
418
+ | Rule | Valid | Invalid |
419
+ |------|-------|---------|
420
+ | Lowercase only | `my-channel` | `My-Channel` |
421
+ | Letters, numbers, hyphens | `channel-123` | `channel:123` |
422
+ | No special characters | `my-channel` | `my_channel`, `my.channel` |
423
+ | No leading/trailing hyphens | `my-channel` | `-channel`, `channel-` |
424
+ | No consecutive hyphens | `my-channel` | `my--channel` |
425
+ | 1-64 characters | `a` to 64 chars | empty or >64 chars |
426
+
427
+ ### Validation Utilities
428
+
429
+ ```typescript
430
+ import {
431
+ isValidChannelSlug,
432
+ toChannelSlug,
433
+ validateChannelSlug,
434
+ } from '@pushflodev/sdk';
435
+
436
+ // Check if valid
437
+ isValidChannelSlug('my-channel'); // true
438
+ isValidChannelSlug('my:channel'); // false
439
+
440
+ // Convert to valid slug
441
+ toChannelSlug('My Channel!'); // 'my-channel'
442
+ toChannelSlug('user:123:messages'); // 'user-123-messages'
443
+
444
+ // Detailed validation with suggestions
445
+ const result = validateChannelSlug('My:Channel');
446
+ // {
447
+ // valid: false,
448
+ // error: "Channel slug contains invalid characters: :. Only lowercase letters, numbers, and hyphens are allowed.",
449
+ // suggestion: "my-channel"
450
+ // }
451
+ ```
452
+
453
+ ### Auto-Creating Channels
454
+
455
+ If you need to auto-create channels with dynamic names, use `toChannelSlug()` to ensure valid slugs:
456
+
457
+ ```typescript
458
+ import { PushFloServer, toChannelSlug } from '@pushflodev/sdk/server';
459
+
460
+ const server = new PushFloServer({ secretKey: 'sec_xxx' });
461
+
462
+ // Convert dynamic names to valid slugs
463
+ const orgId = 'org_abc123';
464
+ const brandId = 'brand_xyz789';
465
+ const channelSlug = toChannelSlug(`${orgId}-${brandId}-notifications`);
466
+ // Result: 'org-abc123-brand-xyz789-notifications'
467
+
468
+ await server.createChannel({
469
+ name: 'Brand Notifications',
470
+ slug: channelSlug,
471
+ });
472
+ ```
412
473
 
413
474
  ## Environment Variables
414
475
 
@@ -77,6 +77,8 @@ declare class PushFloClient extends TypedEventEmitter<PushFloClientEvents> {
77
77
  destroy(): void;
78
78
  /**
79
79
  * Subscribe to a channel
80
+ *
81
+ * @throws {ValidationError} If the channel slug is invalid
80
82
  */
81
83
  subscribe(channel: string, options?: SubscriptionOptions): Subscription;
82
84
  /**
@@ -77,6 +77,8 @@ declare class PushFloClient extends TypedEventEmitter<PushFloClientEvents> {
77
77
  destroy(): void;
78
78
  /**
79
79
  * Subscribe to a channel
80
+ *
81
+ * @throws {ValidationError} If the channel slug is invalid
80
82
  */
81
83
  subscribe(channel: string, options?: SubscriptionOptions): Subscription;
82
84
  /**
@@ -71,6 +71,86 @@ declare class NetworkError extends PushFloError {
71
71
  toJSON(): Record<string, unknown>;
72
72
  }
73
73
 
74
+ /**
75
+ * Error thrown when input validation fails
76
+ */
77
+ declare class ValidationError extends PushFloError {
78
+ /** The field that failed validation */
79
+ readonly field?: string;
80
+ constructor(message: string, field?: string);
81
+ /**
82
+ * Create an error for an invalid channel slug
83
+ */
84
+ static invalidChannelSlug(slug: string): ValidationError;
85
+ /**
86
+ * Create an error for a required field
87
+ */
88
+ static required(field: string): ValidationError;
89
+ toJSON(): Record<string, unknown>;
90
+ }
91
+
92
+ /**
93
+ * Channel slug validation utilities
94
+ *
95
+ * Channel slugs must follow these rules:
96
+ * - 1-64 characters long
97
+ * - Lowercase letters (a-z), numbers (0-9), and hyphens (-) only
98
+ * - Cannot start or end with a hyphen
99
+ * - Cannot have consecutive hyphens
100
+ */
101
+ /** Maximum length for a channel slug */
102
+ declare const MAX_SLUG_LENGTH = 64;
103
+ /** Minimum length for a channel slug */
104
+ declare const MIN_SLUG_LENGTH = 1;
105
+ /**
106
+ * Check if a channel slug is valid
107
+ *
108
+ * @param slug - The channel slug to validate
109
+ * @returns true if the slug is valid, false otherwise
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * isValidChannelSlug('my-channel') // true
114
+ * isValidChannelSlug('channel-123') // true
115
+ * isValidChannelSlug('a') // true
116
+ * isValidChannelSlug('My-Channel') // false (uppercase)
117
+ * isValidChannelSlug('-channel') // false (starts with hyphen)
118
+ * isValidChannelSlug('channel:name') // false (contains colon)
119
+ * isValidChannelSlug('channel_name') // false (contains underscore)
120
+ * ```
121
+ */
122
+ declare function isValidChannelSlug(slug: string): boolean;
123
+ /**
124
+ * Convert a string to a valid channel slug
125
+ *
126
+ * @param str - The string to convert
127
+ * @returns A valid channel slug
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * toChannelSlug('My Channel') // 'my-channel'
132
+ * toChannelSlug('Hello World!') // 'hello-world'
133
+ * toChannelSlug('user:123:messages') // 'user-123-messages'
134
+ * toChannelSlug('___test___') // 'test'
135
+ * ```
136
+ */
137
+ declare function toChannelSlug(str: string): string;
138
+ /**
139
+ * Validation result with detailed error information
140
+ */
141
+ interface SlugValidationResult {
142
+ valid: boolean;
143
+ error?: string;
144
+ suggestion?: string;
145
+ }
146
+ /**
147
+ * Validate a channel slug with detailed error messages
148
+ *
149
+ * @param slug - The channel slug to validate
150
+ * @returns Validation result with error details and suggestions
151
+ */
152
+ declare function validateChannelSlug(slug: string): SlugValidationResult;
153
+
74
154
  /**
75
155
  * A PushFlo channel
76
156
  */
@@ -146,4 +226,4 @@ interface Pagination {
146
226
  totalPages: number;
147
227
  }
148
228
 
149
- export { AuthenticationError as A, type Channel as C, type ListChannelsOptions as L, NetworkError as N, PushFloError as P, type ChannelInput as a, type ChannelUpdateInput as b, type Pagination as c };
229
+ export { AuthenticationError as A, type Channel as C, type ListChannelsOptions as L, MAX_SLUG_LENGTH as M, NetworkError as N, PushFloError as P, type SlugValidationResult as S, ValidationError as V, type ChannelInput as a, type ChannelUpdateInput as b, MIN_SLUG_LENGTH as c, type Pagination as d, isValidChannelSlug as i, toChannelSlug as t, validateChannelSlug as v };
@@ -71,6 +71,86 @@ declare class NetworkError extends PushFloError {
71
71
  toJSON(): Record<string, unknown>;
72
72
  }
73
73
 
74
+ /**
75
+ * Error thrown when input validation fails
76
+ */
77
+ declare class ValidationError extends PushFloError {
78
+ /** The field that failed validation */
79
+ readonly field?: string;
80
+ constructor(message: string, field?: string);
81
+ /**
82
+ * Create an error for an invalid channel slug
83
+ */
84
+ static invalidChannelSlug(slug: string): ValidationError;
85
+ /**
86
+ * Create an error for a required field
87
+ */
88
+ static required(field: string): ValidationError;
89
+ toJSON(): Record<string, unknown>;
90
+ }
91
+
92
+ /**
93
+ * Channel slug validation utilities
94
+ *
95
+ * Channel slugs must follow these rules:
96
+ * - 1-64 characters long
97
+ * - Lowercase letters (a-z), numbers (0-9), and hyphens (-) only
98
+ * - Cannot start or end with a hyphen
99
+ * - Cannot have consecutive hyphens
100
+ */
101
+ /** Maximum length for a channel slug */
102
+ declare const MAX_SLUG_LENGTH = 64;
103
+ /** Minimum length for a channel slug */
104
+ declare const MIN_SLUG_LENGTH = 1;
105
+ /**
106
+ * Check if a channel slug is valid
107
+ *
108
+ * @param slug - The channel slug to validate
109
+ * @returns true if the slug is valid, false otherwise
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * isValidChannelSlug('my-channel') // true
114
+ * isValidChannelSlug('channel-123') // true
115
+ * isValidChannelSlug('a') // true
116
+ * isValidChannelSlug('My-Channel') // false (uppercase)
117
+ * isValidChannelSlug('-channel') // false (starts with hyphen)
118
+ * isValidChannelSlug('channel:name') // false (contains colon)
119
+ * isValidChannelSlug('channel_name') // false (contains underscore)
120
+ * ```
121
+ */
122
+ declare function isValidChannelSlug(slug: string): boolean;
123
+ /**
124
+ * Convert a string to a valid channel slug
125
+ *
126
+ * @param str - The string to convert
127
+ * @returns A valid channel slug
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * toChannelSlug('My Channel') // 'my-channel'
132
+ * toChannelSlug('Hello World!') // 'hello-world'
133
+ * toChannelSlug('user:123:messages') // 'user-123-messages'
134
+ * toChannelSlug('___test___') // 'test'
135
+ * ```
136
+ */
137
+ declare function toChannelSlug(str: string): string;
138
+ /**
139
+ * Validation result with detailed error information
140
+ */
141
+ interface SlugValidationResult {
142
+ valid: boolean;
143
+ error?: string;
144
+ suggestion?: string;
145
+ }
146
+ /**
147
+ * Validate a channel slug with detailed error messages
148
+ *
149
+ * @param slug - The channel slug to validate
150
+ * @returns Validation result with error details and suggestions
151
+ */
152
+ declare function validateChannelSlug(slug: string): SlugValidationResult;
153
+
74
154
  /**
75
155
  * A PushFlo channel
76
156
  */
@@ -146,4 +226,4 @@ interface Pagination {
146
226
  totalPages: number;
147
227
  }
148
228
 
149
- export { AuthenticationError as A, type Channel as C, type ListChannelsOptions as L, NetworkError as N, PushFloError as P, type ChannelInput as a, type ChannelUpdateInput as b, type Pagination as c };
229
+ export { AuthenticationError as A, type Channel as C, type ListChannelsOptions as L, MAX_SLUG_LENGTH as M, NetworkError as N, PushFloError as P, type SlugValidationResult as S, ValidationError as V, type ChannelInput as a, type ChannelUpdateInput as b, MIN_SLUG_LENGTH as c, type Pagination as d, isValidChannelSlug as i, toChannelSlug as t, validateChannelSlug as v };
package/dist/index.cjs CHANGED
@@ -279,6 +279,117 @@ var AuthenticationError = class _AuthenticationError extends PushFloError {
279
279
  }
280
280
  };
281
281
 
282
+ // src/errors/ValidationError.ts
283
+ var ValidationError = class _ValidationError extends PushFloError {
284
+ /** The field that failed validation */
285
+ field;
286
+ constructor(message, field) {
287
+ super(message, "VALIDATION_ERROR", { retryable: false });
288
+ this.name = "ValidationError";
289
+ this.field = field;
290
+ }
291
+ /**
292
+ * Create an error for an invalid channel slug
293
+ */
294
+ static invalidChannelSlug(slug) {
295
+ return new _ValidationError(
296
+ `Invalid channel slug '${slug}': must be 1-64 characters, lowercase alphanumeric with hyphens, cannot start or end with a hyphen`,
297
+ "slug"
298
+ );
299
+ }
300
+ /**
301
+ * Create an error for a required field
302
+ */
303
+ static required(field) {
304
+ return new _ValidationError(`${field} is required`, field);
305
+ }
306
+ toJSON() {
307
+ return {
308
+ ...super.toJSON(),
309
+ field: this.field
310
+ };
311
+ }
312
+ };
313
+
314
+ // src/utils/validation.ts
315
+ var MAX_SLUG_LENGTH = 64;
316
+ var MIN_SLUG_LENGTH = 1;
317
+ var SLUG_REGEX = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
318
+ function isValidChannelSlug(slug) {
319
+ if (!slug || typeof slug !== "string") {
320
+ return false;
321
+ }
322
+ if (slug.length < MIN_SLUG_LENGTH || slug.length > MAX_SLUG_LENGTH) {
323
+ return false;
324
+ }
325
+ if (slug.includes("--")) {
326
+ return false;
327
+ }
328
+ return SLUG_REGEX.test(slug);
329
+ }
330
+ function toChannelSlug(str) {
331
+ return str.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, MAX_SLUG_LENGTH);
332
+ }
333
+ function validateChannelSlug(slug) {
334
+ if (!slug || typeof slug !== "string") {
335
+ return {
336
+ valid: false,
337
+ error: "Channel slug is required"
338
+ };
339
+ }
340
+ if (slug.length < MIN_SLUG_LENGTH) {
341
+ return {
342
+ valid: false,
343
+ error: "Channel slug cannot be empty"
344
+ };
345
+ }
346
+ if (slug.length > MAX_SLUG_LENGTH) {
347
+ return {
348
+ valid: false,
349
+ error: `Channel slug cannot exceed ${MAX_SLUG_LENGTH} characters (got ${slug.length})`,
350
+ suggestion: toChannelSlug(slug)
351
+ };
352
+ }
353
+ if (slug !== slug.toLowerCase()) {
354
+ return {
355
+ valid: false,
356
+ error: "Channel slug must be lowercase",
357
+ suggestion: toChannelSlug(slug)
358
+ };
359
+ }
360
+ if (slug.startsWith("-")) {
361
+ return {
362
+ valid: false,
363
+ error: "Channel slug cannot start with a hyphen",
364
+ suggestion: toChannelSlug(slug)
365
+ };
366
+ }
367
+ if (slug.endsWith("-")) {
368
+ return {
369
+ valid: false,
370
+ error: "Channel slug cannot end with a hyphen",
371
+ suggestion: toChannelSlug(slug)
372
+ };
373
+ }
374
+ if (slug.includes("--")) {
375
+ return {
376
+ valid: false,
377
+ error: "Channel slug cannot contain consecutive hyphens",
378
+ suggestion: toChannelSlug(slug)
379
+ };
380
+ }
381
+ const invalidChars = slug.match(/[^a-z0-9-]/g);
382
+ if (invalidChars) {
383
+ const uniqueChars = [...new Set(invalidChars)].join(", ");
384
+ return {
385
+ valid: false,
386
+ error: `Channel slug contains invalid characters: ${uniqueChars}. Only lowercase letters, numbers, and hyphens are allowed.`,
387
+ suggestion: toChannelSlug(slug)
388
+ };
389
+ }
390
+ return { valid: true };
391
+ }
392
+
282
393
  // src/utils/retry.ts
283
394
  function calculateBackoff(attempt, options = {}) {
284
395
  const {
@@ -1024,11 +1135,16 @@ var PushFloClient = class extends TypedEventEmitter {
1024
1135
  }
1025
1136
  /**
1026
1137
  * Subscribe to a channel
1138
+ *
1139
+ * @throws {ValidationError} If the channel slug is invalid
1027
1140
  */
1028
1141
  subscribe(channel, options = {}) {
1029
1142
  if (!channel) {
1030
1143
  throw new PushFloError("Channel is required", "INVALID_CHANNEL", { retryable: false });
1031
1144
  }
1145
+ if (!isValidChannelSlug(channel)) {
1146
+ throw ValidationError.invalidChannelSlug(channel);
1147
+ }
1032
1148
  this.logger.debug("Subscribing to channel:", channel);
1033
1149
  this.subscriptions.add(channel, options);
1034
1150
  if (this.wsManager.state === "connected") {
@@ -1203,8 +1319,14 @@ var NetworkError = class _NetworkError extends PushFloError {
1203
1319
 
1204
1320
  exports.AuthenticationError = AuthenticationError;
1205
1321
  exports.ConnectionError = ConnectionError;
1322
+ exports.MAX_SLUG_LENGTH = MAX_SLUG_LENGTH;
1323
+ exports.MIN_SLUG_LENGTH = MIN_SLUG_LENGTH;
1206
1324
  exports.NetworkError = NetworkError;
1207
1325
  exports.PushFloClient = PushFloClient;
1208
1326
  exports.PushFloError = PushFloError;
1327
+ exports.ValidationError = ValidationError;
1328
+ exports.isValidChannelSlug = isValidChannelSlug;
1329
+ exports.toChannelSlug = toChannelSlug;
1330
+ exports.validateChannelSlug = validateChannelSlug;
1209
1331
  //# sourceMappingURL=index.cjs.map
1210
1332
  //# sourceMappingURL=index.cjs.map