heroku 10.15.2-beta.0 → 10.16.0-beta.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.
@@ -57,4 +57,5 @@ export declare function reportCmdNotFound(config: any): {
57
57
  };
58
58
  export declare function sendTelemetry(currentTelemetry: any): Promise<void>;
59
59
  export declare function sendToHoneycomb(data: Telemetry | CLIError): Promise<void>;
60
+ export declare function sendToSentry(data: CLIError): Promise<void>;
60
61
  export {};
@@ -1,9 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sendToHoneycomb = exports.sendTelemetry = exports.reportCmdNotFound = exports.computeDuration = exports.setupTelemetry = exports.initializeInstrumentation = exports.processor = void 0;
3
+ exports.sendToSentry = exports.sendToHoneycomb = exports.sendTelemetry = exports.reportCmdNotFound = exports.computeDuration = exports.setupTelemetry = exports.initializeInstrumentation = exports.processor = void 0;
4
4
  const command_1 = require("@heroku-cli/command");
5
5
  const core_1 = require("@oclif/core");
6
6
  const api_1 = require("@opentelemetry/api");
7
+ const Sentry = require("@sentry/node");
8
+ const opentelemetry_1 = require("@sentry/opentelemetry");
9
+ const presets_1 = require("./lib/data-scrubber/presets");
10
+ const scrubber_1 = require("./lib/data-scrubber/scrubber");
11
+ const patterns_1 = require("./lib/data-scrubber/patterns");
7
12
  const { Resource } = require('@opentelemetry/resources');
8
13
  const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
9
14
  const { registerInstrumentations } = require('@opentelemetry/instrumentation');
@@ -24,6 +29,20 @@ const debug = require('debug')('global_telemetry');
24
29
  registerInstrumentations({
25
30
  instrumentations: [],
26
31
  });
32
+ const scrubber = new scrubber_1.Scrubber({
33
+ fields: [...presets_1.HEROKU_FIELDS, ...presets_1.GDPR_FIELDS, ...presets_1.PCI_FIELDS],
34
+ patterns: [...patterns_1.PII_PATTERNS],
35
+ });
36
+ const sentryClient = Sentry.init({
37
+ dsn: 'https://76530569188e7ee2961373f37951d916@o4508609692368896.ingest.us.sentry.io/4508767754846208',
38
+ environment: isDev ? 'development' : 'production',
39
+ release: version,
40
+ tracesSampleRate: 1,
41
+ beforeSend(event) {
42
+ return scrubber.scrub(event).data;
43
+ },
44
+ skipOpenTelemetrySetup: true, // needed since we have our own OTEL setup
45
+ });
27
46
  const resource = Resource
28
47
  .default()
29
48
  .merge(new Resource({
@@ -32,6 +51,7 @@ const resource = Resource
32
51
  }));
33
52
  const provider = new NodeTracerProvider({
34
53
  resource,
54
+ sampler: sentryClient ? new opentelemetry_1.SentrySampler(sentryClient) : undefined,
35
55
  });
36
56
  const headers = { Authorization: `Bearer ${process.env.IS_HEROKU_TEST_ENV !== 'true' ? getToken() : ''}` };
37
57
  const exporter = new OTLPTraceExporter({
@@ -42,7 +62,11 @@ const exporter = new OTLPTraceExporter({
42
62
  exports.processor = new BatchSpanProcessor(exporter);
43
63
  provider.addSpanProcessor(exports.processor);
44
64
  function initializeInstrumentation() {
45
- provider.register();
65
+ provider.register({
66
+ propagator: new opentelemetry_1.SentryPropagator(),
67
+ contextManager: new Sentry.SentryContextManager(),
68
+ });
69
+ // provider.register()
46
70
  }
47
71
  exports.initializeInstrumentation = initializeInstrumentation;
48
72
  function setupTelemetry(config, opts) {
@@ -105,7 +129,15 @@ async function sendTelemetry(currentTelemetry) {
105
129
  return;
106
130
  }
107
131
  const telemetry = currentTelemetry;
108
- await sendToHoneycomb(telemetry);
132
+ if (telemetry instanceof Error) {
133
+ await Promise.all([
134
+ sendToHoneycomb(telemetry),
135
+ sendToSentry(telemetry),
136
+ ]);
137
+ }
138
+ else {
139
+ await sendToHoneycomb(telemetry);
140
+ }
109
141
  }
110
142
  exports.sendTelemetry = sendTelemetry;
111
143
  async function sendToHoneycomb(data) {
@@ -133,10 +165,21 @@ async function sendToHoneycomb(data) {
133
165
  span.setAttribute('heroku_client.lifecycle_hook.command_not_found', data.lifecycleHookCompletion.command_not_found);
134
166
  }
135
167
  span.end();
136
- exports.processor.forceFlush();
168
+ await exports.processor.forceFlush();
137
169
  }
138
170
  catch (_a) {
139
171
  debug('could not send telemetry');
140
172
  }
141
173
  }
142
174
  exports.sendToHoneycomb = sendToHoneycomb;
175
+ async function sendToSentry(data) {
176
+ try {
177
+ Sentry.captureException(data);
178
+ // ensures all events are sent to Sentry before exiting.
179
+ await Sentry.flush();
180
+ }
181
+ catch (_a) {
182
+ debug('Could not send error report');
183
+ }
184
+ }
185
+ exports.sendToSentry = sendToSentry;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Regex patterns for detecting PII in string content
3
+ */
4
+ export declare const PII_PATTERNS: RegExp[];
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PII_PATTERNS = void 0;
4
+ /**
5
+ * Regex patterns for detecting PII in string content
6
+ */
7
+ exports.PII_PATTERNS = [
8
+ // Social Security Numbers (US)
9
+ /\b\d{3}-\d{2}-\d{4}\b/g,
10
+ // Email addresses
11
+ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
12
+ // Phone numbers (US format)
13
+ /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g,
14
+ // JWT tokens
15
+ /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
16
+ ];
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Heroku-specific sensitive field patterns
3
+ *
4
+ * Consolidated list of field names and patterns that contain sensitive data in Heroku applications.
5
+ *
6
+ * Use this preset to ensure consistent PII handling across Heroku services.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { HEROKU_FIELDS } from '@heroku/js-blanket/core/presets';
11
+ * import { Scrubber } from '@heroku/js-blanket';
12
+ *
13
+ * const scrubber = new Scrubber({ fields: HEROKU_FIELDS });
14
+ * const result = scrubber.scrub(data);
15
+ * ```
16
+ */
17
+ export declare const HEROKU_FIELDS: (string | RegExp)[];
18
+ /**
19
+ * GDPR-relevant PII field patterns
20
+ *
21
+ * Field names that typically contain personally identifiable information (PII)
22
+ * regulated by GDPR (General Data Protection Regulation).
23
+ *
24
+ * Use this preset when handling EU user data to ensure compliance with GDPR requirements.
25
+ *
26
+ * @see {@link https://gdpr.eu/what-is-gdpr/|GDPR Official Documentation}
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * import { GDPR_FIELDS, HEROKU_FIELDS } from '@heroku/js-blanket/core/presets';
31
+ * import { Scrubber } from '@heroku/js-blanket';
32
+ *
33
+ * // Combine multiple presets
34
+ * const scrubber = new Scrubber({
35
+ * fields: [...HEROKU_FIELDS, ...GDPR_FIELDS]
36
+ * });
37
+ * ```
38
+ */
39
+ export declare const GDPR_FIELDS: string[];
40
+ /**
41
+ * PCI-DSS relevant field patterns
42
+ *
43
+ * Field names that typically contain payment card information regulated by
44
+ * PCI-DSS (Payment Card Industry Data Security Standard).
45
+ *
46
+ * Use this preset when handling payment card data to help maintain PCI-DSS compliance.
47
+ *
48
+ * **Important**: This preset helps reduce exposure of sensitive payment data in logs and
49
+ * error reports, but is not a substitute for full PCI-DSS compliance measures.
50
+ *
51
+ * @see {@link https://www.pcisecuritystandards.org/|PCI Security Standards Council}
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * import { PCI_FIELDS } from '@heroku/js-blanket/core/presets';
56
+ * import { Scrubber } from '@heroku/js-blanket';
57
+ *
58
+ * const scrubber = new Scrubber({
59
+ * fields: PCI_FIELDS,
60
+ * patterns: [/\d{4}-\d{4}-\d{4}-\d{4}/g] // Also scrub card numbers in text
61
+ * });
62
+ * ```
63
+ */
64
+ export declare const PCI_FIELDS: string[];
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PCI_FIELDS = exports.GDPR_FIELDS = exports.HEROKU_FIELDS = void 0;
4
+ /**
5
+ * Heroku-specific sensitive field patterns
6
+ *
7
+ * Consolidated list of field names and patterns that contain sensitive data in Heroku applications.
8
+ *
9
+ * Use this preset to ensure consistent PII handling across Heroku services.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { HEROKU_FIELDS } from '@heroku/js-blanket/core/presets';
14
+ * import { Scrubber } from '@heroku/js-blanket';
15
+ *
16
+ * const scrubber = new Scrubber({ fields: HEROKU_FIELDS });
17
+ * const result = scrubber.scrub(data);
18
+ * ```
19
+ */
20
+ exports.HEROKU_FIELDS = [
21
+ // Authentication & Sessions
22
+ 'access_token',
23
+ /api[-_]?key/i,
24
+ 'authenticity_token',
25
+ 'heroku_oauth_token',
26
+ 'heroku_session_nonce',
27
+ 'heroku_user_session',
28
+ 'oauth_token',
29
+ 'sudo_oauth_token',
30
+ 'super_user_session_secret',
31
+ 'user_session_secret',
32
+ 'postgres_session_nonce',
33
+ // Passwords & Secrets
34
+ 'password',
35
+ 'passwd',
36
+ 'old_secret',
37
+ 'secret',
38
+ 'secret_token',
39
+ 'confirm_password',
40
+ 'password_confirmation',
41
+ /client[-_]?secret/i,
42
+ // Tokens
43
+ 'token',
44
+ 'bouncer.token',
45
+ 'bouncer.refresh_token',
46
+ // Headers (case-insensitive)
47
+ /authorization/i,
48
+ /cookie/i,
49
+ /x-refresh-token/i,
50
+ // SSO & Sessions
51
+ 'www-sso-session',
52
+ // Payment
53
+ 'payment_method',
54
+ // Infrastructure
55
+ 'logplexUrl',
56
+ ];
57
+ /**
58
+ * GDPR-relevant PII field patterns
59
+ *
60
+ * Field names that typically contain personally identifiable information (PII)
61
+ * regulated by GDPR (General Data Protection Regulation).
62
+ *
63
+ * Use this preset when handling EU user data to ensure compliance with GDPR requirements.
64
+ *
65
+ * @see {@link https://gdpr.eu/what-is-gdpr/|GDPR Official Documentation}
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * import { GDPR_FIELDS, HEROKU_FIELDS } from '@heroku/js-blanket/core/presets';
70
+ * import { Scrubber } from '@heroku/js-blanket';
71
+ *
72
+ * // Combine multiple presets
73
+ * const scrubber = new Scrubber({
74
+ * fields: [...HEROKU_FIELDS, ...GDPR_FIELDS]
75
+ * });
76
+ * ```
77
+ */
78
+ exports.GDPR_FIELDS = [
79
+ 'email',
80
+ 'phone',
81
+ 'address',
82
+ 'postal_code',
83
+ 'ssn',
84
+ 'tax_id',
85
+ ];
86
+ /**
87
+ * PCI-DSS relevant field patterns
88
+ *
89
+ * Field names that typically contain payment card information regulated by
90
+ * PCI-DSS (Payment Card Industry Data Security Standard).
91
+ *
92
+ * Use this preset when handling payment card data to help maintain PCI-DSS compliance.
93
+ *
94
+ * **Important**: This preset helps reduce exposure of sensitive payment data in logs and
95
+ * error reports, but is not a substitute for full PCI-DSS compliance measures.
96
+ *
97
+ * @see {@link https://www.pcisecuritystandards.org/|PCI Security Standards Council}
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * import { PCI_FIELDS } from '@heroku/js-blanket/core/presets';
102
+ * import { Scrubber } from '@heroku/js-blanket';
103
+ *
104
+ * const scrubber = new Scrubber({
105
+ * fields: PCI_FIELDS,
106
+ * patterns: [/\d{4}-\d{4}-\d{4}-\d{4}/g] // Also scrub card numbers in text
107
+ * });
108
+ * ```
109
+ */
110
+ exports.PCI_FIELDS = [
111
+ 'card_number',
112
+ 'cvv',
113
+ 'credit_card',
114
+ 'payment_method',
115
+ ];
@@ -0,0 +1,131 @@
1
+ import { ScrubConfig, ScrubResult } from './types';
2
+ /**
3
+ * Core Scrubber - Deep object traversal with PII scrubbing
4
+ *
5
+ * A high-performance, immutable scrubbing engine that removes sensitive data from structured objects.
6
+ * Supports three scrubbing modes:
7
+ * - **Field-based**: Scrubs values based on field names (e.g., 'password', 'apiToken')
8
+ * - **Path-based**: Scrubs values at specific paths (e.g., 'user.email', 'request.headers.authorization')
9
+ * - **Pattern-based**: Scrubs content matching regex patterns (e.g., SSN, credit cards)
10
+ *
11
+ * ### Design Principles
12
+ * - **Immutable**: All operations create new objects, never mutate inputs
13
+ * - **Type-safe**: Preserves TypeScript types through generic constraints
14
+ * - **Circular-safe**: Handles circular references without crashing
15
+ * - **Performance**: <1ms p95 for logging, <10ms p95 for exception handling (544k+ ops/sec)
16
+ *
17
+ * ### Pattern Adoption
18
+ * Patterns adopted from `@heroku/oauth-provider-adapters-for-mcp/src/logging/redaction.ts`:
19
+ * - Deep recursive traversal with circular reference detection
20
+ * - Immutable cloning strategy with fallback for complex objects
21
+ * - Nested path resolution (e.g., 'user.profile.email')
22
+ * - General array path handling (e.g., 'users[0].password')
23
+ * - Type-safe generics preserving input types
24
+ *
25
+ * Enhanced with:
26
+ * - Field-based matching supporting both strings and regular expressions
27
+ * - Pattern-based content scrubbing for SSN, credit cards, etc.
28
+ * - Dual scrubbing: both field/path matching AND content pattern replacement
29
+ *
30
+ * @example Basic Usage
31
+ * ```typescript
32
+ * const scrubber = new Scrubber({
33
+ * fields: ['password', 'apiToken'],
34
+ * replacement: '[REDACTED]'
35
+ * });
36
+ *
37
+ * const result = scrubber.scrub({
38
+ * user: { name: 'John', password: 'secret123' }
39
+ * });
40
+ * // Result: { user: { name: 'John', password: '[REDACTED]' } }
41
+ * ```
42
+ *
43
+ * @example Advanced Usage with All Modes
44
+ * ```typescript
45
+ * const scrubber = new Scrubber({
46
+ * fields: ['password', /api[-_]?key/i], // Regex matches api_key, api-key, apikey
47
+ * paths: ['user.email', 'request.headers.authorization'],
48
+ * patterns: [/\b\d{3}-\d{2}-\d{4}\b/g], // SSN pattern
49
+ * replacement: '[SCRUBBED]'
50
+ * });
51
+ *
52
+ * const result = scrubber.scrub({
53
+ * user: { name: 'John', email: 'john@example.com', password: 'secret' },
54
+ * request: { headers: { authorization: 'Bearer token123' } },
55
+ * message: 'User SSN is 123-45-6789'
56
+ * });
57
+ * ```
58
+ */
59
+ export declare class Scrubber {
60
+ private config;
61
+ private circularRefs;
62
+ private pathSet;
63
+ /**
64
+ * Creates a new Scrubber instance with the specified configuration
65
+ *
66
+ * @param config - Scrubbing configuration
67
+ * @param config.fields - Field names to scrub (strings or regex patterns)
68
+ * @param config.paths - Dot-notation paths to scrub (e.g., 'user.email', 'items[0].password')
69
+ * @param config.patterns - Regex patterns for content scrubbing (must include global flag for multiple matches)
70
+ * @param config.replacement - Replacement string for scrubbed values (default: '[SCRUBBED]')
71
+ * @param config.recursive - Whether to recursively scrub nested objects (default: true)
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * const scrubber = new Scrubber({
76
+ * fields: ['password', /api[-_]?key/i],
77
+ * paths: ['user.email'],
78
+ * patterns: [/\b\d{3}-\d{2}-\d{4}\b/g],
79
+ * replacement: '[REDACTED]'
80
+ * });
81
+ * ```
82
+ */
83
+ constructor(config: ScrubConfig);
84
+ /**
85
+ * Scrubs sensitive data from an object
86
+ *
87
+ * This is the main entry point for the scrubbing engine. It performs three types of scrubbing:
88
+ * 1. **Field-based**: Replaces values of fields matching configured field names/patterns
89
+ * 2. **Path-based**: Replaces values at specific dot-notation paths
90
+ * 3. **Pattern-based**: Replaces content within string values matching regex patterns
91
+ *
92
+ * The operation is immutable - the input object is not modified. A deep clone is created
93
+ * and scrubbed values are replaced in the clone.
94
+ *
95
+ * ### Performance Characteristics
96
+ * - Small objects (typical logs): ~0.003ms p95
97
+ * - Medium objects (typical errors): ~0.034ms p95
98
+ * - Large objects (10KB+): ~1.2ms p95
99
+ * - Throughput: 54,000+ events/sec
100
+ *
101
+ * @template T - The type of the input object (preserved in output)
102
+ * @param obj - The object to scrub
103
+ * @returns A result object containing the scrubbed data, whether scrubbing occurred, and which paths were scrubbed
104
+ *
105
+ * @example Basic scrubbing
106
+ * ```typescript
107
+ * const scrubber = new Scrubber({ fields: ['password'] });
108
+ * const result = scrubber.scrub({ user: 'john', password: 'secret' });
109
+ * // result.data === { user: 'john', password: '[SCRUBBED]' }
110
+ * // result.scrubbed === true
111
+ * // result.scrubbedPaths === ['password']
112
+ * ```
113
+ *
114
+ * @example Type preservation
115
+ * ```typescript
116
+ * interface User { name: string; email: string; password: string; }
117
+ * const scrubber = new Scrubber({ fields: ['password', 'email'] });
118
+ * const user: User = { name: 'John', email: 'john@example.com', password: 'secret' };
119
+ * const result = scrubber.scrub(user);
120
+ * // result.data is still typed as User
121
+ * ```
122
+ */
123
+ scrub<T>(obj: T): ScrubResult<T>;
124
+ private scrubObject;
125
+ private scrubValue;
126
+ /**
127
+ * Check if a field name matches any configured sensitive field patterns
128
+ */
129
+ private isSensitiveField;
130
+ private deepClone;
131
+ }
@@ -0,0 +1,258 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Scrubber = void 0;
4
+ /**
5
+ * Core Scrubber - Deep object traversal with PII scrubbing
6
+ *
7
+ * A high-performance, immutable scrubbing engine that removes sensitive data from structured objects.
8
+ * Supports three scrubbing modes:
9
+ * - **Field-based**: Scrubs values based on field names (e.g., 'password', 'apiToken')
10
+ * - **Path-based**: Scrubs values at specific paths (e.g., 'user.email', 'request.headers.authorization')
11
+ * - **Pattern-based**: Scrubs content matching regex patterns (e.g., SSN, credit cards)
12
+ *
13
+ * ### Design Principles
14
+ * - **Immutable**: All operations create new objects, never mutate inputs
15
+ * - **Type-safe**: Preserves TypeScript types through generic constraints
16
+ * - **Circular-safe**: Handles circular references without crashing
17
+ * - **Performance**: <1ms p95 for logging, <10ms p95 for exception handling (544k+ ops/sec)
18
+ *
19
+ * ### Pattern Adoption
20
+ * Patterns adopted from `@heroku/oauth-provider-adapters-for-mcp/src/logging/redaction.ts`:
21
+ * - Deep recursive traversal with circular reference detection
22
+ * - Immutable cloning strategy with fallback for complex objects
23
+ * - Nested path resolution (e.g., 'user.profile.email')
24
+ * - General array path handling (e.g., 'users[0].password')
25
+ * - Type-safe generics preserving input types
26
+ *
27
+ * Enhanced with:
28
+ * - Field-based matching supporting both strings and regular expressions
29
+ * - Pattern-based content scrubbing for SSN, credit cards, etc.
30
+ * - Dual scrubbing: both field/path matching AND content pattern replacement
31
+ *
32
+ * @example Basic Usage
33
+ * ```typescript
34
+ * const scrubber = new Scrubber({
35
+ * fields: ['password', 'apiToken'],
36
+ * replacement: '[REDACTED]'
37
+ * });
38
+ *
39
+ * const result = scrubber.scrub({
40
+ * user: { name: 'John', password: 'secret123' }
41
+ * });
42
+ * // Result: { user: { name: 'John', password: '[REDACTED]' } }
43
+ * ```
44
+ *
45
+ * @example Advanced Usage with All Modes
46
+ * ```typescript
47
+ * const scrubber = new Scrubber({
48
+ * fields: ['password', /api[-_]?key/i], // Regex matches api_key, api-key, apikey
49
+ * paths: ['user.email', 'request.headers.authorization'],
50
+ * patterns: [/\b\d{3}-\d{2}-\d{4}\b/g], // SSN pattern
51
+ * replacement: '[SCRUBBED]'
52
+ * });
53
+ *
54
+ * const result = scrubber.scrub({
55
+ * user: { name: 'John', email: 'john@example.com', password: 'secret' },
56
+ * request: { headers: { authorization: 'Bearer token123' } },
57
+ * message: 'User SSN is 123-45-6789'
58
+ * });
59
+ * ```
60
+ */
61
+ class Scrubber {
62
+ /**
63
+ * Creates a new Scrubber instance with the specified configuration
64
+ *
65
+ * @param config - Scrubbing configuration
66
+ * @param config.fields - Field names to scrub (strings or regex patterns)
67
+ * @param config.paths - Dot-notation paths to scrub (e.g., 'user.email', 'items[0].password')
68
+ * @param config.patterns - Regex patterns for content scrubbing (must include global flag for multiple matches)
69
+ * @param config.replacement - Replacement string for scrubbed values (default: '[SCRUBBED]')
70
+ * @param config.recursive - Whether to recursively scrub nested objects (default: true)
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * const scrubber = new Scrubber({
75
+ * fields: ['password', /api[-_]?key/i],
76
+ * paths: ['user.email'],
77
+ * patterns: [/\b\d{3}-\d{2}-\d{4}\b/g],
78
+ * replacement: '[REDACTED]'
79
+ * });
80
+ * ```
81
+ */
82
+ constructor(config) {
83
+ this.circularRefs = new WeakSet();
84
+ this.config = {
85
+ fields: config.fields || [],
86
+ paths: config.paths || [],
87
+ patterns: config.patterns || [],
88
+ replacement: config.replacement || '[SCRUBBED]',
89
+ recursive: config.recursive !== undefined ? config.recursive : true,
90
+ };
91
+ // Pre-compute path set for O(1) lookups
92
+ this.pathSet = new Set(this.config.paths);
93
+ }
94
+ /**
95
+ * Scrubs sensitive data from an object
96
+ *
97
+ * This is the main entry point for the scrubbing engine. It performs three types of scrubbing:
98
+ * 1. **Field-based**: Replaces values of fields matching configured field names/patterns
99
+ * 2. **Path-based**: Replaces values at specific dot-notation paths
100
+ * 3. **Pattern-based**: Replaces content within string values matching regex patterns
101
+ *
102
+ * The operation is immutable - the input object is not modified. A deep clone is created
103
+ * and scrubbed values are replaced in the clone.
104
+ *
105
+ * ### Performance Characteristics
106
+ * - Small objects (typical logs): ~0.003ms p95
107
+ * - Medium objects (typical errors): ~0.034ms p95
108
+ * - Large objects (10KB+): ~1.2ms p95
109
+ * - Throughput: 54,000+ events/sec
110
+ *
111
+ * @template T - The type of the input object (preserved in output)
112
+ * @param obj - The object to scrub
113
+ * @returns A result object containing the scrubbed data, whether scrubbing occurred, and which paths were scrubbed
114
+ *
115
+ * @example Basic scrubbing
116
+ * ```typescript
117
+ * const scrubber = new Scrubber({ fields: ['password'] });
118
+ * const result = scrubber.scrub({ user: 'john', password: 'secret' });
119
+ * // result.data === { user: 'john', password: '[SCRUBBED]' }
120
+ * // result.scrubbed === true
121
+ * // result.scrubbedPaths === ['password']
122
+ * ```
123
+ *
124
+ * @example Type preservation
125
+ * ```typescript
126
+ * interface User { name: string; email: string; password: string; }
127
+ * const scrubber = new Scrubber({ fields: ['password', 'email'] });
128
+ * const user: User = { name: 'John', email: 'john@example.com', password: 'secret' };
129
+ * const result = scrubber.scrub(user);
130
+ * // result.data is still typed as User
131
+ * ```
132
+ */
133
+ scrub(obj) {
134
+ const scrubbedPaths = [];
135
+ const cloned = this.deepClone(obj);
136
+ // Reset circular refs tracker for each scrub operation
137
+ this.circularRefs = new WeakSet();
138
+ const scrubbed = this.scrubObject(cloned, '', scrubbedPaths);
139
+ return {
140
+ data: scrubbed,
141
+ scrubbed: scrubbedPaths.length > 0,
142
+ scrubbedPaths,
143
+ };
144
+ }
145
+ scrubObject(obj, path, paths) {
146
+ // Handle circular references
147
+ if (obj && typeof obj === 'object') {
148
+ if (this.circularRefs.has(obj)) {
149
+ return '[Circular Reference]';
150
+ }
151
+ this.circularRefs.add(obj);
152
+ }
153
+ // Handle primitives
154
+ if (obj === null || typeof obj !== 'object') {
155
+ return this.scrubValue(obj, path, paths);
156
+ }
157
+ // Handle arrays
158
+ if (Array.isArray(obj)) {
159
+ return obj.map((item, index) => {
160
+ const indexStr = index.toString();
161
+ const arrayPath = path ? `${path}[${index}]` : indexStr;
162
+ // Check if this specific array index path should be scrubbed
163
+ if (this.pathSet.has(indexStr) || this.pathSet.has(arrayPath)) {
164
+ paths.push(arrayPath);
165
+ return this.config.replacement;
166
+ }
167
+ // Recursively scrub array items
168
+ return this.scrubObject(item, arrayPath, paths);
169
+ });
170
+ }
171
+ // Handle objects - create new object (immutable approach)
172
+ const result = {};
173
+ for (const [key, value] of Object.entries(obj)) {
174
+ const keyPath = path ? `${path}.${key}` : key;
175
+ // Check if this specific path should be scrubbed
176
+ if (this.pathSet.has(key) || this.pathSet.has(keyPath)) {
177
+ result[key] = this.config.replacement;
178
+ paths.push(keyPath);
179
+ continue;
180
+ }
181
+ // Check if key matches sensitive field pattern
182
+ if (this.isSensitiveField(key)) {
183
+ result[key] = this.config.replacement;
184
+ paths.push(keyPath);
185
+ continue;
186
+ }
187
+ // Recursively scrub value
188
+ result[key] = this.config.recursive ?
189
+ this.scrubObject(value, keyPath, paths) :
190
+ this.scrubValue(value, keyPath, paths);
191
+ }
192
+ return result;
193
+ }
194
+ scrubValue(value, path, paths) {
195
+ if (typeof value !== 'string') {
196
+ return value;
197
+ }
198
+ let scrubbed = value;
199
+ let didScrub = false;
200
+ // Check against patterns (SSN, credit cards, etc.)
201
+ for (const pattern of this.config.patterns) {
202
+ if (pattern.test(scrubbed)) {
203
+ scrubbed = scrubbed.replace(pattern, this.config.replacement);
204
+ didScrub = true;
205
+ }
206
+ }
207
+ if (didScrub) {
208
+ paths.push(path);
209
+ }
210
+ return scrubbed;
211
+ }
212
+ /**
213
+ * Check if a field name matches any configured sensitive field patterns
214
+ */
215
+ isSensitiveField(key) {
216
+ return this.config.fields.some(field => {
217
+ if (field instanceof RegExp) {
218
+ return field.test(key);
219
+ }
220
+ return key.toLowerCase().includes(field.toLowerCase());
221
+ });
222
+ }
223
+ deepClone(obj) {
224
+ try {
225
+ // Fast path for JSON-serializable objects
226
+ return JSON.parse(JSON.stringify(obj));
227
+ }
228
+ catch (_a) {
229
+ // Fallback for objects with circular references
230
+ const seen = new WeakMap();
231
+ // eslint-disable-next-line no-inner-declarations
232
+ function clone(value) {
233
+ if (value === null || typeof value !== 'object') {
234
+ return value;
235
+ }
236
+ if (seen.has(value)) {
237
+ return seen.get(value);
238
+ }
239
+ if (Array.isArray(value)) {
240
+ const arr = [];
241
+ seen.set(value, arr);
242
+ value.forEach((item, i) => {
243
+ arr[i] = clone(item);
244
+ });
245
+ return arr;
246
+ }
247
+ const obj = {};
248
+ seen.set(value, obj);
249
+ Object.keys(value).forEach(key => {
250
+ obj[key] = clone(value[key]);
251
+ });
252
+ return obj;
253
+ }
254
+ return clone(obj);
255
+ }
256
+ }
257
+ }
258
+ exports.Scrubber = Scrubber;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Configuration for the Scrubber
3
+ *
4
+ * Defines how the scrubber should identify and replace sensitive data.
5
+ * Supports three complementary scrubbing strategies:
6
+ *
7
+ * 1. **Field-based scrubbing** (`fields`): Matches field names at any depth in the object tree
8
+ * 2. **Path-based scrubbing** (`paths`): Matches specific dot-notation paths
9
+ * 3. **Pattern-based scrubbing** (`patterns`): Matches regex patterns in string content
10
+ *
11
+ * All three strategies can be used together for comprehensive data scrubbing.
12
+ *
13
+ * @example Field-based configuration
14
+ * ```typescript
15
+ * const config: ScrubConfig = {
16
+ * fields: ['password', 'apiToken', /api[-_]?key/i], // Strings and regex patterns
17
+ * replacement: '[REDACTED]'
18
+ * };
19
+ * ```
20
+ *
21
+ * @example Path-based configuration
22
+ * ```typescript
23
+ * const config: ScrubConfig = {
24
+ * paths: [
25
+ * 'user.email',
26
+ * 'request.headers.authorization',
27
+ * 'items[0].password' // Array index notation
28
+ * ]
29
+ * };
30
+ * ```
31
+ *
32
+ * @example Pattern-based configuration
33
+ * ```typescript
34
+ * const config: ScrubConfig = {
35
+ * patterns: [
36
+ * /\b\d{3}-\d{2}-\d{4}\b/g, // SSN
37
+ * /\d{4}-\d{4}-\d{4}-\d{4}/g, // Credit card
38
+ * /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}/g // Email
39
+ * ]
40
+ * };
41
+ * ```
42
+ */
43
+ export interface ScrubConfig {
44
+ /**
45
+ * Field-based scrubbing: matches field names at any depth
46
+ *
47
+ * Supports both exact string matches and regular expressions for flexible matching.
48
+ * String matches are case-insensitive and use substring matching.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * fields: [
53
+ * 'password', // Matches 'password', 'Password', 'old_password', etc.
54
+ * 'apiToken', // Matches 'apiToken', 'api_token', etc.
55
+ * /api[-_]?key/i, // Regex: matches 'api_key', 'api-key', 'apikey' (case insensitive)
56
+ * /^secret$/ // Exact match: only 'secret', not 'my_secret'
57
+ * ]
58
+ * ```
59
+ */
60
+ fields?: (string | RegExp)[];
61
+ /**
62
+ * Path-based scrubbing: matches specific dot-notation paths
63
+ *
64
+ * Use dot notation to target specific fields in nested objects.
65
+ * Supports array index notation (e.g., `items[0].password`).
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * paths: [
70
+ * 'user.email', // Scrubs obj.user.email
71
+ * 'request.headers.authorization', // Nested path
72
+ * 'items[0].secret', // Array index notation
73
+ * 'users[0]' // Scrubs entire array element
74
+ * ]
75
+ * ```
76
+ */
77
+ paths?: string[];
78
+ /**
79
+ * Pattern-based scrubbing: regex patterns for content scrubbing
80
+ *
81
+ * Scans string values and replaces content matching the patterns.
82
+ * Use the global flag (`/pattern/g`) to replace all matches in a string.
83
+ *
84
+ * **Note**: Patterns are applied to string values only, not to field names or paths.
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * patterns: [
89
+ * /\b\d{3}-\d{2}-\d{4}\b/g, // Social Security Number
90
+ * /\d{4}-\d{4}-\d{4}-\d{4}/g, // Credit Card
91
+ * /Bearer\s+[A-Za-z0-9._-]+/g // Bearer tokens
92
+ * ]
93
+ * ```
94
+ */
95
+ patterns?: RegExp[];
96
+ /**
97
+ * Replacement string for scrubbed values
98
+ *
99
+ * @default '[SCRUBBED]'
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * replacement: '[REDACTED]' // Custom replacement text
104
+ * replacement: '***' // Simple masking
105
+ * replacement: '' // Empty string (removes content)
106
+ * ```
107
+ */
108
+ replacement?: string;
109
+ /**
110
+ * Whether to recursively scrub nested objects
111
+ *
112
+ * When `true`, the scrubber traverses the entire object tree.
113
+ * When `false`, only top-level fields are scrubbed.
114
+ *
115
+ * @default true
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * recursive: false // Only scrub top-level fields
120
+ * ```
121
+ */
122
+ recursive?: boolean;
123
+ }
124
+ /**
125
+ * Result of a scrub operation
126
+ *
127
+ * Contains the scrubbed data along with metadata about what was scrubbed.
128
+ *
129
+ * @template T - The type of the scrubbed data (same as input type)
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * const scrubber = new Scrubber({ fields: ['password'] });
134
+ * const result = scrubber.scrub({ user: 'john', password: 'secret' });
135
+ *
136
+ * console.log(result.data); // { user: 'john', password: '[SCRUBBED]' }
137
+ * console.log(result.scrubbed); // true
138
+ * console.log(result.scrubbedPaths); // ['password']
139
+ * ```
140
+ */
141
+ export interface ScrubResult<T> {
142
+ /**
143
+ * The scrubbed data with sensitive values replaced
144
+ *
145
+ * This is a deep clone of the input with scrubbed values replaced.
146
+ * The original input is never mutated.
147
+ */
148
+ data: T;
149
+ /**
150
+ * Whether any scrubbing occurred
151
+ *
152
+ * `true` if at least one value was scrubbed, `false` if no sensitive data was found.
153
+ *
154
+ * Useful for logging or metrics to track scrubbing activity.
155
+ */
156
+ scrubbed: boolean;
157
+ /**
158
+ * Array of paths that were scrubbed
159
+ *
160
+ * Contains dot-notation paths for all fields that were scrubbed.
161
+ * Useful for debugging, auditing, or understanding what data was redacted.
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * ['password', 'user.email', 'request.headers.authorization']
166
+ * ```
167
+ */
168
+ scrubbedPaths: string[];
169
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,8 +1,8 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
3
  import { SpawnOptions } from 'child_process';
4
- import { ConnectionDetailsWithAttachment } from '@heroku/heroku-cli-util';
5
- export declare function fetchVersion(db: ConnectionDetailsWithAttachment): Promise<string | undefined>;
4
+ import { ConnectionDetails } from '@heroku/heroku-cli-util';
5
+ export declare function fetchVersion(db: ConnectionDetails): Promise<string | undefined>;
6
6
  export declare function psqlFileOptions(file: string, dbEnv: NodeJS.ProcessEnv): {
7
7
  dbEnv: NodeJS.ProcessEnv;
8
8
  psqlArgs: string[];
@@ -13,5 +13,5 @@ export declare function psqlInteractiveOptions(prompt: string, dbEnv: NodeJS.Pro
13
13
  psqlArgs: string[];
14
14
  childProcessOptions: SpawnOptions;
15
15
  };
16
- export declare function execFile(db: ConnectionDetailsWithAttachment, file: string): Promise<string>;
17
- export declare function interactive(db: ConnectionDetailsWithAttachment): Promise<string>;
16
+ export declare function execFile(db: ConnectionDetails, file: string): Promise<string>;
17
+ export declare function interactive(db: ConnectionDetails): Promise<string>;
@@ -15015,5 +15015,5 @@
15015
15015
  ]
15016
15016
  }
15017
15017
  },
15018
- "version": "10.15.2-beta.0"
15018
+ "version": "10.16.0-beta.0"
15019
15019
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "heroku",
3
3
  "description": "CLI to interact with Heroku",
4
- "version": "10.15.2-beta.0",
4
+ "version": "10.16.0-beta.0",
5
5
  "author": "Heroku",
6
6
  "bin": "./bin/run",
7
7
  "bugs": "https://github.com/heroku/cli/issues",
@@ -9,11 +9,11 @@
9
9
  "@heroku-cli/color": "2.0.1",
10
10
  "@heroku-cli/command": "^11.8.0",
11
11
  "@heroku-cli/notifications": "^1.2.4",
12
- "@heroku-cli/plugin-ps-exec": "2.6.2",
12
+ "@heroku-cli/plugin-ps-exec": "2.6.4",
13
13
  "@heroku-cli/schema": "^1.0.25",
14
14
  "@heroku/buildpack-registry": "^1.0.1",
15
15
  "@heroku/eventsource": "^1.0.7",
16
- "@heroku/heroku-cli-util": "^9.1.3",
16
+ "@heroku/heroku-cli-util": "^9.2.0",
17
17
  "@heroku/http-call": "^5.5.0",
18
18
  "@heroku/mcp-server": "1.0.7-alpha.1",
19
19
  "@heroku/plugin-ai": "^1.0.1",
@@ -35,6 +35,8 @@
35
35
  "@opentelemetry/sdk-trace-base": "^1.15.1",
36
36
  "@opentelemetry/sdk-trace-node": "^1.15.1",
37
37
  "@opentelemetry/semantic-conventions": "^1.24.1",
38
+ "@sentry/node": "^10.27.0",
39
+ "@sentry/opentelemetry": "^10.27.0",
38
40
  "@types/js-yaml": "^3.12.5",
39
41
  "ansi-escapes": "3.2.0",
40
42
  "async-file": "^2.0.2",
@@ -61,7 +63,7 @@
61
63
  "printf": "0.6.1",
62
64
  "psl": "^1.9.0",
63
65
  "redis-parser": "^3.0.0",
64
- "semver": "7.6.1",
66
+ "semver": "^7.7.3",
65
67
  "shell-escape": "^0.2.0",
66
68
  "shell-quote": "^1.8.1",
67
69
  "smooth-progress": "^1.1.0",
@@ -397,5 +399,5 @@
397
399
  "version": "oclif readme --multi && git add README.md ../../docs"
398
400
  },
399
401
  "types": "lib/index.d.ts",
400
- "gitHead": "ed1c1a07c328d6b18d1feee24c3ff6ebf0153187"
402
+ "gitHead": "5491b546df92d9877b65266e323ada98a85406f3"
401
403
  }