@chipmobilesdk/rn-ads 0.5.0 → 0.5.1

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.
@@ -1,234 +1,230 @@
1
- /**
2
- * Normalized error model (FR-053) and redaction (NFR-006).
3
- *
4
- * Codes are stable public API: renaming one is a breaking change under LIB-008.
5
- * They exist so a consuming app branches on `code` and `retryable` rather than
6
- * string-matching a provider message that may change under it.
7
- *
8
- * `cause` is a redacted string, never the provider's error object. A provider
9
- * object reaching a consumer would be a contract leak (FR-002) and a potential
10
- * carrier for identifiers the package promises never to emit (FR-021).
11
- */
12
-
13
- import type { AdFormat } from '../config/types';
14
-
15
- export type AdsOperation = 'initialize' | 'load' | 'show' | 'configure';
16
-
17
- export type AdsErrorCode =
18
- // Configuration — none retryable; the configuration has to change.
19
- | 'invalid_config'
20
- | 'duplicate_placement_key'
21
- | 'missing_ad_unit_id'
22
- | 'format_mismatch'
23
- | 'placement_not_found'
24
- | 'production_in_dev_build'
25
- | 'timeout_out_of_bounds'
26
- // Availability — none retryable within this process.
27
- | 'unsupported_platform'
28
- | 'provider_missing'
29
- | 'disabled'
30
- // The app has not decided whether ads are on. Sibling of `disabled`, and
31
- // non-retryable for the same reason: calling again changes nothing until the
32
- // app resolves it (FR-115).
33
- | 'undecided'
34
- // Privacy — resolved by consent changing, not by calling again.
35
- | 'consent_required'
36
- | 'consent_undetermined'
37
- | 'requires_reinitialization'
38
- // The package's own consent flow has not produced permission. Distinct from
39
- // `consent_required`, which means the app's own gate is closed — a refusal
40
- // has to name which of the two inputs is blocking (FR-105a).
41
- | 'consent_flow_required'
42
- // The consent flow failed. Never retried by the package: FR-135 excludes
43
- // consent errors, and re-running the flow is the app's call, not a backoff's.
44
- | 'consent_flow_failed'
45
- // Runtime.
46
- | 'no_fill'
47
- | 'network_error'
48
- | 'load_timeout'
49
- | 'show_timeout'
50
- | 'ad_expired'
51
- | 'not_ready'
52
- | 'busy'
53
- | 'consumed'
54
- | 'app_inactive'
55
- // The user came back to the app from an ad, or from a browser an ad opened.
56
- // Only the package can know this, which is why refusing it is a capability
57
- // rather than a policy (FR-128). Not retryable: retry covers load, and this
58
- // is a show refusal that resolves with time rather than with another attempt.
59
- | 'ad_originated_return'
60
- | 'provider_error'
61
- | 'internal_error'
62
- // Configuration — a retry policy outside its bounds. Rejected rather than
63
- // clamped, matching `timeout_out_of_bounds`: silently clamping tells the app
64
- // it configured something it did not.
65
- | 'retry_out_of_bounds';
66
-
67
- /**
68
- * "Retrying this operation could plausibly succeed" — not "retry immediately".
69
- *
70
- * 0.2.0 added an opt-in retry policy, and this set is what it is allowed to act
71
- * on. Everything outside it is excluded deliberately: configuration and consent
72
- * errors, an unsupported platform, and a disabled or undecided instance are all
73
- * states another attempt cannot change, so retrying them would spend a billable
74
- * request on a certainty (FR-135).
75
- */
76
- const RETRYABLE: ReadonlySet<AdsErrorCode> = new Set<AdsErrorCode>([
77
- 'no_fill',
78
- 'network_error',
79
- 'load_timeout',
80
- 'show_timeout',
81
- 'ad_expired',
82
- 'not_ready',
83
- 'busy',
84
- 'consumed',
85
- 'app_inactive',
86
- 'provider_error',
87
- ]);
88
-
89
- export interface AdsErrorOptions {
90
- readonly message?: string;
91
- readonly operation?: AdsOperation;
92
- readonly placementKey?: string;
93
- readonly format?: AdFormat;
94
- readonly cause?: unknown;
95
- }
96
-
97
- export class AdsError extends Error {
98
- readonly code: AdsErrorCode;
99
- readonly retryable: boolean;
100
- readonly operation?: AdsOperation;
101
- readonly placementKey?: string;
102
- readonly format?: AdFormat;
103
- /** Redacted provider detail. Never a provider object. */
104
- readonly cause?: string;
105
-
106
- constructor(code: AdsErrorCode, options: AdsErrorOptions = {}) {
107
- super(options.message ?? code);
108
- this.name = 'AdsError';
109
- this.code = code;
110
- this.retryable = RETRYABLE.has(code);
111
- this.operation = options.operation;
112
- this.placementKey = options.placementKey;
113
- this.format = options.format;
114
- this.cause = options.cause === undefined ? undefined : redact(options.cause);
115
- Object.setPrototypeOf(this, AdsError.prototype);
116
- }
117
- }
118
-
119
- /**
120
- * Configuration faults are thrown rather than returned, because they mean the
121
- * app is wired wrong and no runtime result could describe a usable outcome.
122
- * Every other failure in the package is a typed result (FR-004).
123
- */
124
- export class AdsConfigError extends AdsError {
125
- constructor(code: AdsErrorCode, options: AdsErrorOptions = {}) {
126
- super(code, { ...options, operation: options.operation ?? 'configure' });
127
- this.name = 'AdsConfigError';
128
- Object.setPrototypeOf(this, AdsConfigError.prototype);
129
- }
130
- }
131
-
132
- export function isAdsError(value: unknown): value is AdsError {
133
- return value instanceof AdsError;
134
- }
135
-
136
- const REDACTED = '[redacted]';
137
-
138
- /** An AdMob unit or app id. Redacted so a log never carries a live identifier. */
139
- const AD_UNIT_PATTERN = /ca-app-pub-\d+[~/]\d+/g;
140
- /** GAID and any other dashed UUID an SDK might put in a message. */
141
- const UUID_PATTERN =
142
- /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g;
143
- /** Base64-ish runs long enough to be an encoded payload such as a consent string. */
144
- const PAYLOAD_PATTERN = /[A-Za-z0-9+/=_-]{40,}/g;
145
- /** Anything path- or URL-like, which can carry query parameters with identifiers. */
146
- const URL_PATTERN = /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s"']+/g;
147
-
148
- /**
149
- * Keys dropped wholesale rather than pattern-matched, because their values are
150
- * sensitive regardless of shape — a bare advertising id has no separator for a
151
- * pattern to catch.
152
- */
153
- const SENSITIVE_KEYS = new Set([
154
- 'adid',
155
- 'adUnitId',
156
- 'advertisingId',
157
- 'consent',
158
- 'consentString',
159
- 'gaid',
160
- 'idfa',
161
- 'tcString',
162
- 'token',
163
- 'userId',
164
- ]);
165
-
166
- function redactString(value: string): string {
167
- return value
168
- .replace(URL_PATTERN, REDACTED)
169
- .replace(AD_UNIT_PATTERN, REDACTED)
170
- .replace(UUID_PATTERN, REDACTED)
171
- .replace(PAYLOAD_PATTERN, REDACTED);
172
- }
173
-
174
- /**
175
- * Reduces an arbitrary provider value to a safe string (FR-053, NFR-006).
176
- *
177
- * Returns a string rather than a structure on purpose: a redacted object still
178
- * hands the consumer a reference into provider-owned memory, and the contract
179
- * promises no provider object crosses the boundary.
180
- */
181
- export function redact(value: unknown, depth = 0): string {
182
- if (value === null || value === undefined) {
183
- return '';
184
- }
185
- if (typeof value === 'string') {
186
- return redactString(value);
187
- }
188
- if (typeof value === 'number' || typeof value === 'boolean') {
189
- return String(value);
190
- }
191
- if (value instanceof Error) {
192
- return redactString(value.message);
193
- }
194
- if (depth >= 2) {
195
- return REDACTED;
196
- }
197
- if (Array.isArray(value)) {
198
- return value.map(item => redact(item, depth + 1)).join(', ');
199
- }
200
- if (typeof value === 'object') {
201
- const parts: string[] = [];
202
- for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
203
- if (SENSITIVE_KEYS.has(key)) {
204
- parts.push(`${key}=${REDACTED}`);
205
- continue;
206
- }
207
- parts.push(`${key}=${redact(entry, depth + 1)}`);
208
- }
209
- return parts.join(' ');
210
- }
211
- return REDACTED;
212
- }
213
-
214
- /**
215
- * Maps a provider failure onto the normalized taxonomy. Provider codes vary by
216
- * SDK version, so the match is on substrings that have been stable across
217
- * releases, and anything unrecognized becomes a retryable `provider_error`
218
- * rather than being silently reshaped into a code that means something specific.
219
- */
220
- export function normalizeProviderError(
221
- value: unknown,
222
- options: AdsErrorOptions = {},
223
- ): AdsError {
224
- const text = value instanceof Error ? value.message : String(value ?? '');
225
- const lowered = text.toLowerCase();
226
-
227
- if (lowered.includes('no-fill') || lowered.includes('no fill')) {
228
- return new AdsError('no_fill', { ...options, cause: value });
229
- }
230
- if (lowered.includes('network')) {
231
- return new AdsError('network_error', { ...options, cause: value });
232
- }
233
- return new AdsError('provider_error', { ...options, cause: value });
234
- }
1
+ /**
2
+ * Normalized error model (FR-053) and redaction (NFR-006).
3
+ *
4
+ * Codes are stable public API: renaming one is a breaking change under LIB-008.
5
+ * They exist so a consuming app branches on `code` and `retryable` rather than
6
+ * string-matching a provider message that may change under it.
7
+ *
8
+ * `cause` is a redacted string, never the provider's error object. A provider
9
+ * object reaching a consumer would be a contract leak (FR-002) and a potential
10
+ * carrier for identifiers the package promises never to emit (FR-021).
11
+ */
12
+
13
+ import type { AdFormat } from '../config/types';
14
+
15
+ export type AdsOperation = 'initialize' | 'load' | 'show' | 'configure';
16
+
17
+ export type AdsErrorCode =
18
+ // Configuration — none retryable; the configuration has to change.
19
+ | 'invalid_config'
20
+ | 'duplicate_placement_key'
21
+ | 'missing_ad_unit_id'
22
+ | 'format_mismatch'
23
+ | 'placement_not_found'
24
+ | 'production_in_dev_build'
25
+ | 'timeout_out_of_bounds'
26
+ // Availability — none retryable within this process.
27
+ | 'unsupported_platform'
28
+ | 'provider_missing'
29
+ | 'disabled'
30
+ // The app has not decided whether ads are on. Sibling of `disabled`, and
31
+ // non-retryable for the same reason: calling again changes nothing until the
32
+ // app resolves it (FR-115).
33
+ | 'undecided'
34
+ // Privacy — resolved by consent changing, not by calling again.
35
+ | 'consent_required'
36
+ | 'consent_undetermined'
37
+ | 'requires_reinitialization'
38
+ // The package's own consent flow has not produced permission. Distinct from
39
+ // `consent_required`, which means the app's own gate is closed — a refusal
40
+ // has to name which of the two inputs is blocking (FR-105a).
41
+ | 'consent_flow_required'
42
+ // The consent flow failed. Never retried by the package: FR-135 excludes
43
+ // consent errors, and re-running the flow is the app's call, not a backoff's.
44
+ | 'consent_flow_failed'
45
+ // Runtime.
46
+ | 'no_fill'
47
+ | 'network_error'
48
+ | 'load_timeout'
49
+ | 'show_timeout'
50
+ | 'ad_expired'
51
+ | 'not_ready'
52
+ | 'busy'
53
+ | 'consumed'
54
+ | 'app_inactive'
55
+ // The user came back to the app from an ad, or from a browser an ad opened.
56
+ // Only the package can know this, which is why refusing it is a capability
57
+ // rather than a policy (FR-128). Not retryable: retry covers load, and this
58
+ // is a show refusal that resolves with time rather than with another attempt.
59
+ | 'ad_originated_return'
60
+ | 'provider_error'
61
+ | 'internal_error'
62
+ // Configuration — a retry policy outside its bounds. Rejected rather than
63
+ // clamped, matching `timeout_out_of_bounds`: silently clamping tells the app
64
+ // it configured something it did not.
65
+ | 'retry_out_of_bounds';
66
+
67
+ /**
68
+ * "Retrying this operation could plausibly succeed" — not "retry immediately".
69
+ *
70
+ * 0.2.0 added an opt-in retry policy, and this set is what it is allowed to act
71
+ * on. Everything outside it is excluded deliberately: configuration and consent
72
+ * errors, an unsupported platform, and a disabled or undecided instance are all
73
+ * states another attempt cannot change, so retrying them would spend a billable
74
+ * request on a certainty (FR-135).
75
+ */
76
+ const RETRYABLE: ReadonlySet<AdsErrorCode> = new Set<AdsErrorCode>([
77
+ 'no_fill',
78
+ 'network_error',
79
+ 'load_timeout',
80
+ 'show_timeout',
81
+ 'ad_expired',
82
+ 'not_ready',
83
+ 'busy',
84
+ 'consumed',
85
+ 'app_inactive',
86
+ 'provider_error',
87
+ ]);
88
+
89
+ export interface AdsErrorOptions {
90
+ readonly message?: string;
91
+ readonly operation?: AdsOperation;
92
+ readonly placementKey?: string;
93
+ readonly format?: AdFormat;
94
+ readonly cause?: unknown;
95
+ }
96
+
97
+ export class AdsError extends Error {
98
+ readonly code: AdsErrorCode;
99
+ readonly retryable: boolean;
100
+ readonly operation?: AdsOperation;
101
+ readonly placementKey?: string;
102
+ readonly format?: AdFormat;
103
+ /** Redacted provider detail. Never a provider object. */
104
+ readonly cause?: string;
105
+
106
+ constructor(code: AdsErrorCode, options: AdsErrorOptions = {}) {
107
+ super(options.message ?? code);
108
+ this.name = 'AdsError';
109
+ this.code = code;
110
+ this.retryable = RETRYABLE.has(code);
111
+ this.operation = options.operation;
112
+ this.placementKey = options.placementKey;
113
+ this.format = options.format;
114
+ this.cause = options.cause === undefined ? undefined : redact(options.cause);
115
+ Object.setPrototypeOf(this, AdsError.prototype);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Configuration faults are thrown rather than returned, because they mean the
121
+ * app is wired wrong and no runtime result could describe a usable outcome.
122
+ * Every other failure in the package is a typed result (FR-004).
123
+ */
124
+ export class AdsConfigError extends AdsError {
125
+ constructor(code: AdsErrorCode, options: AdsErrorOptions = {}) {
126
+ super(code, { ...options, operation: options.operation ?? 'configure' });
127
+ this.name = 'AdsConfigError';
128
+ Object.setPrototypeOf(this, AdsConfigError.prototype);
129
+ }
130
+ }
131
+
132
+ export function isAdsError(value: unknown): value is AdsError {
133
+ return value instanceof AdsError;
134
+ }
135
+
136
+ const REDACTED = '[redacted]';
137
+
138
+ /** An AdMob unit or app id. Redacted so a log never carries a live identifier. */
139
+ const AD_UNIT_PATTERN = /ca-app-pub-\d+[~/]\d+/g;
140
+ /** GAID and any other dashed UUID an SDK might put in a message. */
141
+ const UUID_PATTERN = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g;
142
+ /** Base64-ish runs long enough to be an encoded payload such as a consent string. */
143
+ const PAYLOAD_PATTERN = /[A-Za-z0-9+/=_-]{40,}/g;
144
+ /** Anything path- or URL-like, which can carry query parameters with identifiers. */
145
+ const URL_PATTERN = /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s"']+/g;
146
+
147
+ /**
148
+ * Keys dropped wholesale rather than pattern-matched, because their values are
149
+ * sensitive regardless of shape — a bare advertising id has no separator for a
150
+ * pattern to catch.
151
+ */
152
+ const SENSITIVE_KEYS = new Set([
153
+ 'adid',
154
+ 'adUnitId',
155
+ 'advertisingId',
156
+ 'consent',
157
+ 'consentString',
158
+ 'gaid',
159
+ 'idfa',
160
+ 'tcString',
161
+ 'token',
162
+ 'userId',
163
+ ]);
164
+
165
+ function redactString(value: string): string {
166
+ return value
167
+ .replace(URL_PATTERN, REDACTED)
168
+ .replace(AD_UNIT_PATTERN, REDACTED)
169
+ .replace(UUID_PATTERN, REDACTED)
170
+ .replace(PAYLOAD_PATTERN, REDACTED);
171
+ }
172
+
173
+ /**
174
+ * Reduces an arbitrary provider value to a safe string (FR-053, NFR-006).
175
+ *
176
+ * Returns a string rather than a structure on purpose: a redacted object still
177
+ * hands the consumer a reference into provider-owned memory, and the contract
178
+ * promises no provider object crosses the boundary.
179
+ */
180
+ export function redact(value: unknown, depth = 0): string {
181
+ if (value === null || value === undefined) {
182
+ return '';
183
+ }
184
+ if (typeof value === 'string') {
185
+ return redactString(value);
186
+ }
187
+ if (typeof value === 'number' || typeof value === 'boolean') {
188
+ return String(value);
189
+ }
190
+ if (value instanceof Error) {
191
+ return redactString(value.message);
192
+ }
193
+ if (depth >= 2) {
194
+ return REDACTED;
195
+ }
196
+ if (Array.isArray(value)) {
197
+ return value.map(item => redact(item, depth + 1)).join(', ');
198
+ }
199
+ if (typeof value === 'object') {
200
+ const parts: string[] = [];
201
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
202
+ if (SENSITIVE_KEYS.has(key)) {
203
+ parts.push(`${key}=${REDACTED}`);
204
+ continue;
205
+ }
206
+ parts.push(`${key}=${redact(entry, depth + 1)}`);
207
+ }
208
+ return parts.join(' ');
209
+ }
210
+ return REDACTED;
211
+ }
212
+
213
+ /**
214
+ * Maps a provider failure onto the normalized taxonomy. Provider codes vary by
215
+ * SDK version, so the match is on substrings that have been stable across
216
+ * releases, and anything unrecognized becomes a retryable `provider_error`
217
+ * rather than being silently reshaped into a code that means something specific.
218
+ */
219
+ export function normalizeProviderError(value: unknown, options: AdsErrorOptions = {}): AdsError {
220
+ const text = value instanceof Error ? value.message : String(value ?? '');
221
+ const lowered = text.toLowerCase();
222
+
223
+ if (lowered.includes('no-fill') || lowered.includes('no fill')) {
224
+ return new AdsError('no_fill', { ...options, cause: value });
225
+ }
226
+ if (lowered.includes('network')) {
227
+ return new AdsError('network_error', { ...options, cause: value });
228
+ }
229
+ return new AdsError('provider_error', { ...options, cause: value });
230
+ }