adstxt-validator 1.2.3
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/LICENSE +21 -0
- package/README.md +806 -0
- package/dist/index.d.ts +238 -0
- package/dist/index.js +1273 -0
- package/dist/locales/en/validation.json +89 -0
- package/dist/locales/ja/validation.json +89 -0
- package/dist/messages.d.ts +263 -0
- package/dist/messages.js +189 -0
- package/package.json +55 -0
- package/src/locales/en/validation.json +89 -0
- package/src/locales/ja/validation.json +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
# @miyaichi/ads-txt-validator
|
|
2
|
+
|
|
3
|
+
A comprehensive TypeScript library for parsing, validating, and cross-checking ads.txt files against sellers.json data. This package provides robust validation capabilities with detailed error reporting and optimization features.
|
|
4
|
+
|
|
5
|
+
> **Note**: This package is available on GitHub Packages. For internal use within the adstxt-manager monorepo, the package is also available as `@adstxt-manager/ads-txt-validator`.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Complete ads.txt parsing**: Parse ads.txt files and extract records and variables
|
|
10
|
+
- **Sellers.json cross-checking**: Validate ads.txt entries against sellers.json specifications
|
|
11
|
+
- **Duplicate detection**: Identify duplicate entries across ads.txt files
|
|
12
|
+
- **Content optimization**: Remove duplicates and standardize format
|
|
13
|
+
- **Comprehensive validation**: Multiple validation levels with detailed error reporting
|
|
14
|
+
- **Internationalized messages**: Multi-language support with configurable help URLs
|
|
15
|
+
- **External URL configuration**: Configure base URLs for help links when used as external library
|
|
16
|
+
- **TypeScript support**: Full TypeScript support with detailed type definitions
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
### From GitHub Packages
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @miyaichi/ads-txt-validator
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### From internal monorepo
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @adstxt-manager/ads-txt-validator
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
### Basic Usage
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { parseAdsTxtContent, crossCheckAdsTxtRecords } from '@miyaichi/ads-txt-validator';
|
|
38
|
+
|
|
39
|
+
// Parse ads.txt content
|
|
40
|
+
const adsTxtContent = `
|
|
41
|
+
example.com, pub-1234, DIRECT
|
|
42
|
+
reseller.com, reseller-5678, RESELLER
|
|
43
|
+
CONTACT=admin@example.com
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const parsedEntries = parseAdsTxtContent(adsTxtContent, 'example.com');
|
|
47
|
+
|
|
48
|
+
// Legacy approach (still supported)
|
|
49
|
+
const getSellersJson = async (domain: string) => {
|
|
50
|
+
const response = await fetch(`https://${domain}/sellers.json`);
|
|
51
|
+
return response.json();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const validatedEntries = await crossCheckAdsTxtRecords(
|
|
55
|
+
'publisher.com',
|
|
56
|
+
parsedEntries,
|
|
57
|
+
null, // cached ads.txt content
|
|
58
|
+
getSellersJson
|
|
59
|
+
);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Optimized Usage (Recommended)
|
|
63
|
+
|
|
64
|
+
For better performance, especially with large sellers.json files, use the new `SellersJsonProvider` interface:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import {
|
|
68
|
+
parseAdsTxtContent,
|
|
69
|
+
crossCheckAdsTxtRecords,
|
|
70
|
+
SellersJsonProvider,
|
|
71
|
+
} from '@miyaichi/ads-txt-validator';
|
|
72
|
+
|
|
73
|
+
// Create optimized provider
|
|
74
|
+
const sellersJsonProvider: SellersJsonProvider = {
|
|
75
|
+
async batchGetSellers(domain: string, sellerIds: string[]) {
|
|
76
|
+
// Efficiently fetch only needed sellers
|
|
77
|
+
const result = await fetchSellersFromDatabase(domain, sellerIds);
|
|
78
|
+
return {
|
|
79
|
+
domain,
|
|
80
|
+
requested_count: sellerIds.length,
|
|
81
|
+
found_count: result.foundCount,
|
|
82
|
+
results: result.sellers,
|
|
83
|
+
metadata: result.metadata,
|
|
84
|
+
cache: result.cacheInfo,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async hasSellerJson(domain: string) {
|
|
89
|
+
return await checkSellerJsonExists(domain);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async getMetadata(domain: string) {
|
|
93
|
+
return await getSellerJsonMetadata(domain);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
async getCacheInfo(domain: string) {
|
|
97
|
+
return await getCacheInformation(domain);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Use optimized validation
|
|
102
|
+
const validatedEntries = await crossCheckAdsTxtRecords(
|
|
103
|
+
'publisher.com',
|
|
104
|
+
parsedEntries,
|
|
105
|
+
null,
|
|
106
|
+
sellersJsonProvider
|
|
107
|
+
);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## API Reference
|
|
111
|
+
|
|
112
|
+
### Core Functions
|
|
113
|
+
|
|
114
|
+
#### `parseAdsTxtContent(content: string, publisherDomain?: string): ParsedAdsTxtEntry[]`
|
|
115
|
+
|
|
116
|
+
Parses complete ads.txt file content and returns an array of parsed entries.
|
|
117
|
+
|
|
118
|
+
**Parameters:**
|
|
119
|
+
|
|
120
|
+
- `content`: Raw ads.txt file content
|
|
121
|
+
- `publisherDomain`: Optional publisher domain for default OWNERDOMAIN
|
|
122
|
+
|
|
123
|
+
**Returns:** Array of `ParsedAdsTxtEntry` objects
|
|
124
|
+
|
|
125
|
+
#### `parseAdsTxtLine(line: string, lineNumber: number): ParsedAdsTxtEntry | null`
|
|
126
|
+
|
|
127
|
+
Parses a single line from an ads.txt file.
|
|
128
|
+
|
|
129
|
+
**Parameters:**
|
|
130
|
+
|
|
131
|
+
- `line`: Single line from ads.txt file
|
|
132
|
+
- `lineNumber`: Line number for error reporting
|
|
133
|
+
|
|
134
|
+
**Returns:** `ParsedAdsTxtEntry` object or `null` for comments/empty lines
|
|
135
|
+
|
|
136
|
+
#### `crossCheckAdsTxtRecords(publisherDomain: string, parsedEntries: ParsedAdsTxtEntry[], cachedAdsTxtContent: string | null, sellersJsonProvider: SellersJsonProvider): Promise<ParsedAdsTxtEntry[]>`
|
|
137
|
+
|
|
138
|
+
**Optimized cross-check function (recommended)** - Cross-checks parsed entries against existing ads.txt and sellers.json data using efficient selective queries.
|
|
139
|
+
|
|
140
|
+
**Parameters:**
|
|
141
|
+
|
|
142
|
+
- `publisherDomain`: Publisher's domain for validation
|
|
143
|
+
- `parsedEntries`: Array of parsed ads.txt entries
|
|
144
|
+
- `cachedAdsTxtContent`: Existing ads.txt content for duplicate detection
|
|
145
|
+
- `sellersJsonProvider`: Optimized provider for sellers.json data
|
|
146
|
+
|
|
147
|
+
**Returns:** Promise resolving to enhanced entries with validation results
|
|
148
|
+
|
|
149
|
+
#### `crossCheckAdsTxtRecords(publisherDomain: string, parsedEntries: ParsedAdsTxtEntry[], cachedAdsTxtContent: string | null, getSellersJson: (domain: string) => Promise<any>): Promise<ParsedAdsTxtEntry[]>`
|
|
150
|
+
|
|
151
|
+
**Legacy cross-check function** - Cross-checks parsed entries against existing ads.txt and sellers.json data.
|
|
152
|
+
|
|
153
|
+
**Parameters:**
|
|
154
|
+
|
|
155
|
+
- `publisherDomain`: Publisher's domain for validation
|
|
156
|
+
- `parsedEntries`: Array of parsed ads.txt entries
|
|
157
|
+
- `cachedAdsTxtContent`: Existing ads.txt content for duplicate detection
|
|
158
|
+
- `getSellersJson`: Function to fetch complete sellers.json data
|
|
159
|
+
|
|
160
|
+
**Returns:** Promise resolving to enhanced entries with validation results
|
|
161
|
+
|
|
162
|
+
**Note:** This overload is deprecated in favor of the `SellersJsonProvider` version for better performance.
|
|
163
|
+
|
|
164
|
+
#### `optimizeAdsTxt(content: string, publisherDomain?: string): string`
|
|
165
|
+
|
|
166
|
+
Optimizes ads.txt content by removing duplicates and standardizing format.
|
|
167
|
+
|
|
168
|
+
**Parameters:**
|
|
169
|
+
|
|
170
|
+
- `content`: Raw ads.txt content
|
|
171
|
+
- `publisherDomain`: Optional publisher domain
|
|
172
|
+
|
|
173
|
+
**Returns:** Optimized ads.txt content string
|
|
174
|
+
|
|
175
|
+
#### `isValidEmail(email: string): boolean`
|
|
176
|
+
|
|
177
|
+
Validates email addresses with comprehensive regex.
|
|
178
|
+
|
|
179
|
+
**Parameters:**
|
|
180
|
+
|
|
181
|
+
- `email`: Email address to validate
|
|
182
|
+
|
|
183
|
+
**Returns:** Boolean indicating validity
|
|
184
|
+
|
|
185
|
+
### Message System Functions
|
|
186
|
+
|
|
187
|
+
#### `configureMessages(config: MessageConfig): void`
|
|
188
|
+
|
|
189
|
+
Configures the global message provider with base URL and locale settings.
|
|
190
|
+
|
|
191
|
+
**Parameters:**
|
|
192
|
+
|
|
193
|
+
- `config`: Configuration object with optional `defaultLocale` and `baseUrl`
|
|
194
|
+
|
|
195
|
+
#### `createValidationMessage(key: string, placeholders?: string[], locale?: string): ValidationMessage | null`
|
|
196
|
+
|
|
197
|
+
Creates a localized validation message with formatted help URLs.
|
|
198
|
+
|
|
199
|
+
**Parameters:**
|
|
200
|
+
|
|
201
|
+
- `key`: Validation error key (e.g., 'domainMismatch')
|
|
202
|
+
- `placeholders`: Array of values to substitute in message templates
|
|
203
|
+
- `locale`: Target locale (defaults to configured locale)
|
|
204
|
+
|
|
205
|
+
**Returns:** `ValidationMessage` object or null if key not found
|
|
206
|
+
|
|
207
|
+
#### `setMessageProvider(provider: MessageProvider): void`
|
|
208
|
+
|
|
209
|
+
Sets a custom message provider for advanced usage.
|
|
210
|
+
|
|
211
|
+
**Parameters:**
|
|
212
|
+
|
|
213
|
+
- `provider`: Custom message provider implementation
|
|
214
|
+
|
|
215
|
+
#### `getMessageProvider(): MessageProvider`
|
|
216
|
+
|
|
217
|
+
Gets the current message provider.
|
|
218
|
+
|
|
219
|
+
**Returns:** Current `MessageProvider` instance
|
|
220
|
+
|
|
221
|
+
### Type Definitions
|
|
222
|
+
|
|
223
|
+
#### `SellersJsonProvider`
|
|
224
|
+
|
|
225
|
+
Interface for optimized sellers.json data access:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
interface SellersJsonProvider {
|
|
229
|
+
batchGetSellers(domain: string, sellerIds: string[]): Promise<BatchSellersResult>;
|
|
230
|
+
getMetadata(domain: string): Promise<SellersJsonMetadata>;
|
|
231
|
+
hasSellerJson(domain: string): Promise<boolean>;
|
|
232
|
+
getCacheInfo(domain: string): Promise<CacheInfo>;
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### `BatchSellersResult`
|
|
237
|
+
|
|
238
|
+
Result structure for batch seller queries:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
interface BatchSellersResult {
|
|
242
|
+
domain: string;
|
|
243
|
+
requested_count: number;
|
|
244
|
+
found_count: number;
|
|
245
|
+
results: SellerResult[];
|
|
246
|
+
metadata: SellersJsonMetadata;
|
|
247
|
+
cache: CacheInfo;
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### `SellerResult`
|
|
252
|
+
|
|
253
|
+
Individual seller query result:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
interface SellerResult {
|
|
257
|
+
sellerId: string;
|
|
258
|
+
seller: Seller | null;
|
|
259
|
+
found: boolean;
|
|
260
|
+
source: 'cache' | 'fresh';
|
|
261
|
+
error?: string;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### `Seller`
|
|
266
|
+
|
|
267
|
+
Seller information from sellers.json:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
interface Seller {
|
|
271
|
+
seller_id: string;
|
|
272
|
+
name?: string;
|
|
273
|
+
domain?: string;
|
|
274
|
+
seller_type?: 'PUBLISHER' | 'INTERMEDIARY' | 'BOTH';
|
|
275
|
+
is_confidential?: 0 | 1;
|
|
276
|
+
[key: string]: any;
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### `SellersJsonMetadata`
|
|
281
|
+
|
|
282
|
+
Metadata from sellers.json file:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
interface SellersJsonMetadata {
|
|
286
|
+
version?: string;
|
|
287
|
+
contact_email?: string;
|
|
288
|
+
contact_address?: string;
|
|
289
|
+
seller_count?: number;
|
|
290
|
+
identifiers?: any[];
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### `CacheInfo`
|
|
295
|
+
|
|
296
|
+
Cache information:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
interface CacheInfo {
|
|
300
|
+
is_cached: boolean;
|
|
301
|
+
last_updated?: string;
|
|
302
|
+
status: 'success' | 'error' | 'stale';
|
|
303
|
+
expires_at?: string;
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
#### `ParsedAdsTxtEntry`
|
|
308
|
+
|
|
309
|
+
Union type for ads.txt entries:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
type ParsedAdsTxtEntry = ParsedAdsTxtRecord | ParsedAdsTxtVariable;
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
#### `ParsedAdsTxtRecord`
|
|
316
|
+
|
|
317
|
+
Interface for ads.txt records:
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
interface ParsedAdsTxtRecord {
|
|
321
|
+
line_number: number;
|
|
322
|
+
raw_line: string;
|
|
323
|
+
is_valid: boolean;
|
|
324
|
+
domain: string;
|
|
325
|
+
account_id: string;
|
|
326
|
+
account_type: string;
|
|
327
|
+
certification_authority_id?: string;
|
|
328
|
+
relationship: 'DIRECT' | 'RESELLER';
|
|
329
|
+
error?: string;
|
|
330
|
+
has_warning?: boolean;
|
|
331
|
+
warning?: string;
|
|
332
|
+
validation_key?: string;
|
|
333
|
+
severity?: Severity;
|
|
334
|
+
duplicate_domain?: string;
|
|
335
|
+
validation_results?: CrossCheckValidationResult;
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### `ParsedAdsTxtVariable`
|
|
340
|
+
|
|
341
|
+
Interface for ads.txt variables:
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
interface ParsedAdsTxtVariable {
|
|
345
|
+
line_number: number;
|
|
346
|
+
raw_line: string;
|
|
347
|
+
is_valid: boolean;
|
|
348
|
+
variable_type:
|
|
349
|
+
| 'CONTACT'
|
|
350
|
+
| 'SUBDOMAIN'
|
|
351
|
+
| 'INVENTORYPARTNERDOMAIN'
|
|
352
|
+
| 'OWNERDOMAIN'
|
|
353
|
+
| 'MANAGERDOMAIN';
|
|
354
|
+
value: string;
|
|
355
|
+
is_variable: true;
|
|
356
|
+
error?: string;
|
|
357
|
+
has_warning?: boolean;
|
|
358
|
+
warning?: string;
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### `CrossCheckValidationResult`
|
|
363
|
+
|
|
364
|
+
Detailed validation results from sellers.json cross-checking:
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
interface CrossCheckValidationResult {
|
|
368
|
+
hasSellerJson: boolean;
|
|
369
|
+
directAccountIdInSellersJson: boolean;
|
|
370
|
+
directDomainMatchesSellerJsonEntry: boolean | null;
|
|
371
|
+
directEntryHasPublisherType: boolean | null;
|
|
372
|
+
directSellerIdIsUnique: boolean | null;
|
|
373
|
+
resellerAccountIdInSellersJson: boolean | null;
|
|
374
|
+
resellerDomainMatchesSellerJsonEntry: boolean | null;
|
|
375
|
+
resellerEntryHasIntermediaryType: boolean | null;
|
|
376
|
+
resellerSellerIdIsUnique: boolean | null;
|
|
377
|
+
sellerData?: SellersJsonSellerRecord | null;
|
|
378
|
+
error?: string;
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
#### `Severity`
|
|
383
|
+
|
|
384
|
+
Validation severity levels:
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
enum Severity {
|
|
388
|
+
ERROR = 'error',
|
|
389
|
+
WARNING = 'warning',
|
|
390
|
+
INFO = 'info',
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
#### `MessageConfig`
|
|
395
|
+
|
|
396
|
+
Configuration interface for message system:
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
interface MessageConfig {
|
|
400
|
+
defaultLocale?: 'ja' | 'en';
|
|
401
|
+
baseUrl?: string;
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
#### `ValidationMessage`
|
|
406
|
+
|
|
407
|
+
Complete validation message with localized content:
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
interface ValidationMessage {
|
|
411
|
+
key: string;
|
|
412
|
+
severity: Severity;
|
|
413
|
+
message: string;
|
|
414
|
+
description?: string;
|
|
415
|
+
helpUrl?: string;
|
|
416
|
+
placeholders: string[];
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Type Guards
|
|
421
|
+
|
|
422
|
+
#### `isAdsTxtRecord(entry: ParsedAdsTxtEntry): entry is ParsedAdsTxtRecord`
|
|
423
|
+
|
|
424
|
+
Checks if an entry is an ads.txt record.
|
|
425
|
+
|
|
426
|
+
#### `isAdsTxtVariable(entry: ParsedAdsTxtEntry): entry is ParsedAdsTxtVariable`
|
|
427
|
+
|
|
428
|
+
Checks if an entry is an ads.txt variable.
|
|
429
|
+
|
|
430
|
+
## Validation Features
|
|
431
|
+
|
|
432
|
+
### Basic Validation
|
|
433
|
+
|
|
434
|
+
- **Format validation**: Ensures proper comma-separated format
|
|
435
|
+
- **Required fields**: Validates presence of domain, account_id, account_type
|
|
436
|
+
- **Domain validation**: Uses PSL (Public Suffix List) for domain validation
|
|
437
|
+
- **Relationship validation**: Ensures valid DIRECT/RESELLER relationships
|
|
438
|
+
- **Account ID validation**: Checks for non-empty account IDs
|
|
439
|
+
|
|
440
|
+
### Advanced Validation (Sellers.json Cross-checking)
|
|
441
|
+
|
|
442
|
+
The package implements comprehensive sellers.json validation based on IAB standards:
|
|
443
|
+
|
|
444
|
+
- **Case 11/16**: Checks if advertising system has sellers.json file
|
|
445
|
+
- **Case 12**: For DIRECT entries, validates account ID exists in sellers.json
|
|
446
|
+
- **Case 13**: For DIRECT entries, validates domain matching against OWNERDOMAIN/MANAGERDOMAIN
|
|
447
|
+
- **Case 14**: For DIRECT entries, validates seller_type is PUBLISHER
|
|
448
|
+
- **Case 15**: For DIRECT entries, validates seller_id uniqueness
|
|
449
|
+
- **Case 17**: For RESELLER entries, validates account ID exists in sellers.json
|
|
450
|
+
- **Case 18**: For RESELLER entries, validates domain matching
|
|
451
|
+
- **Case 19**: For RESELLER entries, validates seller_type is INTERMEDIARY
|
|
452
|
+
- **Case 20**: For RESELLER entries, validates seller_id uniqueness
|
|
453
|
+
|
|
454
|
+
### Duplicate Detection
|
|
455
|
+
|
|
456
|
+
- Detects duplicate entries between submitted and existing ads.txt files
|
|
457
|
+
- Uses normalized comparison (case-insensitive domains, exact account IDs)
|
|
458
|
+
- Marks duplicates with INFO severity warnings
|
|
459
|
+
|
|
460
|
+
## Performance Optimization
|
|
461
|
+
|
|
462
|
+
### SellersJsonProvider vs Legacy Approach
|
|
463
|
+
|
|
464
|
+
The new `SellersJsonProvider` interface offers significant performance improvements over the legacy approach:
|
|
465
|
+
|
|
466
|
+
| Aspect | Legacy Approach | SellersJsonProvider |
|
|
467
|
+
| -------------------- | ---------------------------------- | ---------------------------------- |
|
|
468
|
+
| **Memory Usage** | Loads entire sellers.json (100MB+) | Loads only needed sellers (few KB) |
|
|
469
|
+
| **Network Transfer** | Downloads complete files | Selective database queries |
|
|
470
|
+
| **Processing Time** | O(n) linear search | O(log n) indexed lookups |
|
|
471
|
+
| **Scalability** | Poor for large files | Excellent for any size |
|
|
472
|
+
|
|
473
|
+
### Performance Metrics
|
|
474
|
+
|
|
475
|
+
For a typical sellers.json file with 10,000 sellers:
|
|
476
|
+
|
|
477
|
+
- **Memory reduction**: 99%+ (100MB → 5KB)
|
|
478
|
+
- **Query speed**: 50-100x faster
|
|
479
|
+
- **Network bandwidth**: 95%+ reduction
|
|
480
|
+
|
|
481
|
+
### When to Use Each Approach
|
|
482
|
+
|
|
483
|
+
**Use SellersJsonProvider when:**
|
|
484
|
+
|
|
485
|
+
- Working with large sellers.json files (>1MB)
|
|
486
|
+
- Performance is critical
|
|
487
|
+
- Database/cache infrastructure is available
|
|
488
|
+
- Processing multiple ads.txt files
|
|
489
|
+
|
|
490
|
+
**Use Legacy approach when:**
|
|
491
|
+
|
|
492
|
+
- Simple one-off validations
|
|
493
|
+
- No database infrastructure
|
|
494
|
+
- Working with small sellers.json files
|
|
495
|
+
- Backward compatibility is required
|
|
496
|
+
|
|
497
|
+
## Error Handling
|
|
498
|
+
|
|
499
|
+
The package uses comprehensive error keys for different validation scenarios:
|
|
500
|
+
|
|
501
|
+
- `MISSING_FIELDS`: Missing required fields
|
|
502
|
+
- `INVALID_FORMAT`: Invalid line format
|
|
503
|
+
- `INVALID_RELATIONSHIP`: Invalid relationship type
|
|
504
|
+
- `INVALID_DOMAIN`: Invalid domain format
|
|
505
|
+
- `EMPTY_ACCOUNT_ID`: Empty account ID
|
|
506
|
+
- `IMPLIMENTED`: Duplicate entry detected
|
|
507
|
+
- `NO_SELLERS_JSON`: Missing sellers.json file
|
|
508
|
+
- `DIRECT_ACCOUNT_ID_NOT_IN_SELLERS_JSON`: Direct account not in sellers.json
|
|
509
|
+
- `RESELLER_ACCOUNT_ID_NOT_IN_SELLERS_JSON`: Reseller account not in sellers.json
|
|
510
|
+
- `DOMAIN_MISMATCH`: Domain mismatch with sellers.json
|
|
511
|
+
- `DIRECT_NOT_PUBLISHER`: Direct entry not marked as publisher
|
|
512
|
+
- `SELLER_ID_NOT_UNIQUE`: Seller ID appears multiple times
|
|
513
|
+
- `RESELLER_NOT_INTERMEDIARY`: Reseller not marked as intermediary
|
|
514
|
+
|
|
515
|
+
## Examples
|
|
516
|
+
|
|
517
|
+
### Basic Parsing
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
import { parseAdsTxtContent } from '@miyaichi/ads-txt-validator';
|
|
521
|
+
|
|
522
|
+
const adsTxtContent = `
|
|
523
|
+
# Ads.txt file
|
|
524
|
+
example.com, pub-1234, DIRECT, f08c47fec0942fa0
|
|
525
|
+
reseller.com, reseller-5678, RESELLER
|
|
526
|
+
CONTACT=admin@example.com
|
|
527
|
+
OWNERDOMAIN=example.com
|
|
528
|
+
`;
|
|
529
|
+
|
|
530
|
+
const parsedEntries = parseAdsTxtContent(adsTxtContent, 'example.com');
|
|
531
|
+
|
|
532
|
+
// Filter only valid records
|
|
533
|
+
const validRecords = parsedEntries.filter((entry) => entry.is_valid).filter(isAdsTxtRecord);
|
|
534
|
+
|
|
535
|
+
console.log(`Found ${validRecords.length} valid records`);
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Cross-checking with Sellers.json
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
import { crossCheckAdsTxtRecords, parseAdsTxtContent } from '@miyaichi/ads-txt-validator';
|
|
542
|
+
|
|
543
|
+
const getSellersJson = async (domain: string) => {
|
|
544
|
+
try {
|
|
545
|
+
const response = await fetch(`https://${domain}/sellers.json`);
|
|
546
|
+
return await response.json();
|
|
547
|
+
} catch (error) {
|
|
548
|
+
return null; // Handle fetch errors
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const parsedEntries = parseAdsTxtContent(adsTxtContent, 'publisher.com');
|
|
553
|
+
|
|
554
|
+
const validatedEntries = await crossCheckAdsTxtRecords(
|
|
555
|
+
'publisher.com',
|
|
556
|
+
parsedEntries,
|
|
557
|
+
existingAdsTxtContent,
|
|
558
|
+
getSellersJson
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
// Check validation results
|
|
562
|
+
validatedEntries.forEach((entry) => {
|
|
563
|
+
if (entry.has_warning) {
|
|
564
|
+
console.warn(`Warning for ${entry.raw_line}: ${entry.warning}`);
|
|
565
|
+
}
|
|
566
|
+
if (entry.validation_results) {
|
|
567
|
+
console.log('Validation details:', entry.validation_results);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### Configuring Help URLs for External Applications
|
|
573
|
+
|
|
574
|
+
When using this package as an external library, you can configure the base URL for help links:
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
import { configureMessages, createValidationMessage } from '@miyaichi/ads-txt-validator';
|
|
578
|
+
|
|
579
|
+
// Configure the message system with your application's base URL
|
|
580
|
+
configureMessages({
|
|
581
|
+
defaultLocale: 'ja', // or 'en'
|
|
582
|
+
baseUrl: 'https://your-app.com',
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// Now validation messages will have complete URLs
|
|
586
|
+
const message = createValidationMessage('domainMismatch', ['example.com', 'google.com']);
|
|
587
|
+
console.log(message.helpUrl);
|
|
588
|
+
// Output: https://your-app.com/help/#domain-mismatch
|
|
589
|
+
|
|
590
|
+
// For deployment environments, use environment variables
|
|
591
|
+
if (process.env.APP_URL) {
|
|
592
|
+
configureMessages({
|
|
593
|
+
defaultLocale: 'ja',
|
|
594
|
+
baseUrl: process.env.APP_URL,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
#### Help URL Generation
|
|
600
|
+
|
|
601
|
+
The package automatically generates help URLs based on validation keys using the following format:
|
|
602
|
+
|
|
603
|
+
```
|
|
604
|
+
{baseUrl}/help/#{validation-key}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
**Validation Key to URL Fragment Mapping:**
|
|
608
|
+
|
|
609
|
+
| Validation Key | URL Fragment | Description |
|
|
610
|
+
|----------------|--------------|-------------|
|
|
611
|
+
| `missingFields` | `missing-fields` | Missing required fields error |
|
|
612
|
+
| `invalidFormat` | `invalid-format` | Invalid line format error |
|
|
613
|
+
| `invalidRelationship` | `invalid-relationship` | Invalid relationship type |
|
|
614
|
+
| `invalidDomain` | `invalid-domain` | Invalid domain format |
|
|
615
|
+
| `emptyAccountId` | `empty-account-id` | Empty account ID error |
|
|
616
|
+
| `noValidEntries` | `no-valid-entries` | No valid entries found |
|
|
617
|
+
| `whitespaceInFields` | `whitespace-in-fields` | Whitespace in fields error |
|
|
618
|
+
| `implimented` | `implimented-entry` | Duplicate entry warning |
|
|
619
|
+
| `noSellersJson` | `no-sellers-json` | Missing sellers.json file |
|
|
620
|
+
| `directAccountIdNotInSellersJson` | `direct-account-id-not-in-sellers-json` | Direct account not in sellers.json |
|
|
621
|
+
| `resellerAccountIdNotInSellersJson` | `reseller-account-id-not-in-sellers-json` | Reseller account not in sellers.json |
|
|
622
|
+
| `domainMismatch` | `domain-mismatch` | Domain mismatch with sellers.json |
|
|
623
|
+
| `directNotPublisher` | `direct-not-publisher` | Direct entry not marked as publisher |
|
|
624
|
+
| `sellerIdNotUnique` | `seller-id-not-unique` | Seller ID not unique |
|
|
625
|
+
| `resellerNotIntermediary` | `reseller-not-intermediary` | Reseller not marked as intermediary |
|
|
626
|
+
|
|
627
|
+
#### Implementing Help Pages
|
|
628
|
+
|
|
629
|
+
To implement help pages in your application, create a help page that supports URL fragments:
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
// Example React component for help page
|
|
633
|
+
import React, { useEffect } from 'react';
|
|
634
|
+
import { useLocation } from 'react-router-dom';
|
|
635
|
+
|
|
636
|
+
export const HelpPage: React.FC = () => {
|
|
637
|
+
const location = useLocation();
|
|
638
|
+
|
|
639
|
+
useEffect(() => {
|
|
640
|
+
// Highlight section based on URL fragment
|
|
641
|
+
const fragment = location.hash.replace('#', '');
|
|
642
|
+
if (fragment) {
|
|
643
|
+
const element = document.getElementById(fragment);
|
|
644
|
+
if (element) {
|
|
645
|
+
element.scrollIntoView({ behavior: 'smooth' });
|
|
646
|
+
// Add highlight effect
|
|
647
|
+
element.classList.add('highlight-section');
|
|
648
|
+
setTimeout(() => {
|
|
649
|
+
element.classList.remove('highlight-section');
|
|
650
|
+
}, 5000);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}, [location.hash]);
|
|
654
|
+
|
|
655
|
+
return (
|
|
656
|
+
<div>
|
|
657
|
+
<h1>Ads.txt Validation Help</h1>
|
|
658
|
+
|
|
659
|
+
<section id="domain-mismatch">
|
|
660
|
+
<h2>Domain Mismatch</h2>
|
|
661
|
+
<p>This error occurs when the domain in sellers.json doesn't match...</p>
|
|
662
|
+
</section>
|
|
663
|
+
|
|
664
|
+
<section id="missing-fields">
|
|
665
|
+
<h2>Missing Fields</h2>
|
|
666
|
+
<p>Ads.txt entries require at least three fields...</p>
|
|
667
|
+
</section>
|
|
668
|
+
|
|
669
|
+
{/* Add more sections for each validation key */}
|
|
670
|
+
</div>
|
|
671
|
+
);
|
|
672
|
+
};
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
#### Static Help Page Example
|
|
676
|
+
|
|
677
|
+
For static HTML applications, you can implement fragment-based navigation:
|
|
678
|
+
|
|
679
|
+
```html
|
|
680
|
+
<!DOCTYPE html>
|
|
681
|
+
<html>
|
|
682
|
+
<head>
|
|
683
|
+
<title>Ads.txt Validation Help</title>
|
|
684
|
+
<style>
|
|
685
|
+
.highlight-section {
|
|
686
|
+
background-color: rgba(255, 193, 7, 0.3);
|
|
687
|
+
border-radius: 4px;
|
|
688
|
+
padding: 8px;
|
|
689
|
+
margin: -8px;
|
|
690
|
+
transition: all 0.3s ease-in-out;
|
|
691
|
+
animation: highlight-fade 5s ease-out forwards;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
@keyframes highlight-fade {
|
|
695
|
+
0% { background-color: rgba(255, 193, 7, 0.4); }
|
|
696
|
+
100% { background-color: transparent; }
|
|
697
|
+
}
|
|
698
|
+
</style>
|
|
699
|
+
</head>
|
|
700
|
+
<body>
|
|
701
|
+
<h1>Ads.txt Validation Help</h1>
|
|
702
|
+
|
|
703
|
+
<section>
|
|
704
|
+
<a id="domain-mismatch"></a>
|
|
705
|
+
<h2>Domain Mismatch</h2>
|
|
706
|
+
<p>This error occurs when...</p>
|
|
707
|
+
</section>
|
|
708
|
+
|
|
709
|
+
<script>
|
|
710
|
+
// Handle fragment highlighting
|
|
711
|
+
function highlightSection() {
|
|
712
|
+
const fragment = window.location.hash.replace('#', '');
|
|
713
|
+
if (fragment) {
|
|
714
|
+
const element = document.getElementById(fragment);
|
|
715
|
+
if (element) {
|
|
716
|
+
element.scrollIntoView({ behavior: 'smooth' });
|
|
717
|
+
|
|
718
|
+
// If it's an empty anchor, highlight the next heading
|
|
719
|
+
let elementToHighlight = element;
|
|
720
|
+
if (element.tagName === 'A' && !element.textContent.trim()) {
|
|
721
|
+
const nextHeading = element.nextElementSibling;
|
|
722
|
+
if (nextHeading && nextHeading.tagName.match(/^H[1-6]$/)) {
|
|
723
|
+
elementToHighlight = nextHeading;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
elementToHighlight.classList.add('highlight-section');
|
|
728
|
+
setTimeout(() => {
|
|
729
|
+
elementToHighlight.classList.remove('highlight-section');
|
|
730
|
+
}, 5000);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Highlight on page load and hash change
|
|
736
|
+
window.addEventListener('load', highlightSection);
|
|
737
|
+
window.addEventListener('hashchange', highlightSection);
|
|
738
|
+
</script>
|
|
739
|
+
</body>
|
|
740
|
+
</html>
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### Content Optimization
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
import { optimizeAdsTxt } from '@miyaichi/ads-txt-validator';
|
|
747
|
+
|
|
748
|
+
const messyAdsTxtContent = `
|
|
749
|
+
# Ads.txt file
|
|
750
|
+
example.com, pub-1234, DIRECT
|
|
751
|
+
example.com, pub-1234, DIRECT
|
|
752
|
+
CONTACT=admin@example.com
|
|
753
|
+
reseller.com, reseller-5678, RESELLER
|
|
754
|
+
CONTACT=admin@example.com
|
|
755
|
+
`;
|
|
756
|
+
|
|
757
|
+
const optimizedContent = optimizeAdsTxt(messyAdsTxtContent, 'publisher.com');
|
|
758
|
+
console.log(optimizedContent);
|
|
759
|
+
// Output will have duplicates removed and content organized
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### Error Handling
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
import { parseAdsTxtContent, isAdsTxtRecord } from '@miyaichi/ads-txt-validator';
|
|
766
|
+
|
|
767
|
+
const parsedEntries = parseAdsTxtContent(adsTxtContent);
|
|
768
|
+
|
|
769
|
+
parsedEntries.forEach((entry) => {
|
|
770
|
+
if (!entry.is_valid) {
|
|
771
|
+
console.error(`Line ${entry.line_number}: ${entry.error}`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (entry.has_warning) {
|
|
775
|
+
console.warn(`Line ${entry.line_number}: ${entry.warning}`);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (isAdsTxtRecord(entry) && entry.validation_results) {
|
|
779
|
+
if (!entry.validation_results.hasSellerJson) {
|
|
780
|
+
console.warn(`No sellers.json found for ${entry.domain}`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
## Dependencies
|
|
787
|
+
|
|
788
|
+
- `psl`: Public Suffix List for domain validation
|
|
789
|
+
|
|
790
|
+
## Development
|
|
791
|
+
|
|
792
|
+
```bash
|
|
793
|
+
# Build the package
|
|
794
|
+
npm run build
|
|
795
|
+
|
|
796
|
+
# Run tests
|
|
797
|
+
npm test
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
## License
|
|
801
|
+
|
|
802
|
+
MIT
|
|
803
|
+
|
|
804
|
+
## Contributing
|
|
805
|
+
|
|
806
|
+
Contributions are welcome! Please ensure all tests pass and follow the existing code style.
|