@aamp/protocol 1.1.4 → 1.1.6

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/src/publisher.ts CHANGED
@@ -2,14 +2,17 @@
2
2
  * Layer 2: Publisher Middleware
3
3
  * Used by content owners to enforce policy, log access, and filter bots.
4
4
  */
5
- import { HEADERS, MAX_CLOCK_SKEW_MS, WELL_KNOWN_AGENT_PATH } from './constants';
6
- import { exportPublicKey, signData, verifySignature } from './crypto';
7
- import { AccessPolicy, AccessPurpose, AgentIdentityManifest, ContentOrigin, EvaluationResult, IdentityCache, SignedAccessRequest, UnauthenticatedStrategy } from './types';
5
+ import { HEADERS, MAX_CLOCK_SKEW_MS, WELL_KNOWN_AGENT_PATH } from './constants.js';
6
+ import { exportPublicKey, signData, verifySignature } from './crypto.js';
7
+ import { AccessPolicy, AccessPurpose, AgentIdentityManifest, ContentOrigin, EvaluationResult, IdentityCache, SignedAccessRequest, UnauthenticatedStrategy } from './types.js';
8
+ import { verifyJwt } from './proof.js';
8
9
 
9
10
  interface VerificationResult {
10
11
  allowed: boolean;
11
12
  reason: string;
12
13
  identityVerified: boolean;
14
+ proofUsed?: string; // "WHITELIST", "CREDENTIAL_JWT", "AD_JWT"
15
+ visitorType?: string; // For audit logs
13
16
  }
14
17
 
15
18
  /**
@@ -18,7 +21,7 @@ interface VerificationResult {
18
21
  */
19
22
  class MemoryCache implements IdentityCache {
20
23
  private store = new Map<string, { val: string, exp: number }>();
21
-
24
+
22
25
  async get(key: string): Promise<string | null> {
23
26
  const item = this.store.get(key);
24
27
  if (!item) return null;
@@ -28,11 +31,11 @@ class MemoryCache implements IdentityCache {
28
31
  }
29
32
  return item.val;
30
33
  }
31
-
34
+
32
35
  async set(key: string, value: string, ttlSeconds: number): Promise<void> {
33
- this.store.set(key, {
34
- val: value,
35
- exp: Date.now() + (ttlSeconds * 1000)
36
+ this.store.set(key, {
37
+ val: value,
38
+ exp: Date.now() + (ttlSeconds * 1000)
36
39
  });
37
40
  }
38
41
  }
@@ -42,14 +45,14 @@ export class AAMPPublisher {
42
45
  private keyPair: CryptoKeyPair | null = null;
43
46
  private unauthenticatedStrategy: UnauthenticatedStrategy;
44
47
  private cache: IdentityCache;
45
-
48
+
46
49
  // Default TTL: 1 Hour
47
- private readonly CACHE_TTL_SECONDS = 3600;
50
+ private readonly CACHE_TTL_SECONDS = 3600;
48
51
 
49
52
  constructor(
50
- policy: AccessPolicy,
53
+ policy: AccessPolicy,
51
54
  strategy: UnauthenticatedStrategy = 'PASSIVE',
52
- cacheImpl?: IdentityCache
55
+ cacheImpl?: IdentityCache
53
56
  ) {
54
57
  this.policy = policy;
55
58
  this.unauthenticatedStrategy = strategy;
@@ -68,14 +71,20 @@ export class AAMPPublisher {
68
71
  * Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
69
72
  */
70
73
  async evaluateVisitor(
71
- reqHeaders: Record<string, string | undefined>,
74
+ reqHeaders: Record<string, string | undefined>,
72
75
  rawPayload?: string
73
76
  ): Promise<EvaluationResult> {
74
-
77
+
75
78
  // 1. Check for AAMP Headers
76
79
  const hasAamp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
77
80
 
81
+ const feedbackToken = reqHeaders[HEADERS.FEEDBACK];
82
+ if (feedbackToken) {
83
+ await this.handleFeedback(feedbackToken, reqHeaders);
84
+ }
85
+
78
86
  if (hasAamp) {
87
+ console.log("\nšŸ”Ž [AAMP Middleware] Detected Agent Headers. Starting Verification...");
79
88
  // It claims to be an Agent. Verify it.
80
89
  return await this.handleAgent(reqHeaders, rawPayload);
81
90
  }
@@ -95,13 +104,13 @@ export class AAMPPublisher {
95
104
  allowed: true,
96
105
  status: 200,
97
106
  reason: "PASSIVE_MODE: Allowed without verification.",
98
- visitorType: 'LIKELY_HUMAN'
107
+ visitorType: 'LIKELY_HUMAN'
99
108
  };
100
109
  }
101
110
 
102
111
  // 3. HYBRID MODE: Heuristic Analysis (The "Lazy Bot" Filter)
103
112
  const isHuman = this.performBrowserHeuristics(reqHeaders);
104
-
113
+
105
114
  if (isHuman) {
106
115
  return {
107
116
  allowed: true,
@@ -127,38 +136,27 @@ export class AAMPPublisher {
127
136
  */
128
137
  private performBrowserHeuristics(headers: Record<string, string | undefined>): boolean {
129
138
  const userAgent = headers['user-agent'] || '';
130
-
139
+
131
140
  // A. The "Obvious Bot" Blocklist (Fast Fail)
132
141
  const botSignatures = ['python-requests', 'curl', 'wget', 'scrapy', 'bot', 'crawler', 'spider'];
133
- // Exception: Googlebot (if you want SEO). We'll treat Googlebot as a bot,
134
- // real implementations might white-list it via IP verification (not possible in just JS headers).
135
142
  if (botSignatures.some(sig => userAgent.toLowerCase().includes(sig))) {
136
- return false;
143
+ return false;
137
144
  }
138
145
 
139
146
  // B. Trusted Infrastructure Signals (The Real World Solution)
140
- // If Cloudflare or Vercel says "This is a real user", we trust them.
141
- // Cloudflare: 'cf-visitor' exists. 'cf-ipcountry' exists.
142
147
  if (headers['cf-visitor'] || headers['cf-ray']) return true;
143
-
144
- // Vercel: 'x-vercel-id'
145
148
  if (headers['x-vercel-id']) return true;
146
-
147
- // AWS CloudFront: 'cloudfront-viewer-address'
148
149
  if (headers['cloudfront-viewer-address']) return true;
149
150
 
150
151
  // C. The "Browser Fingerprint" (Fallback for direct connections)
151
- // Real browsers almost always send these headers
152
152
  const hasAcceptLanguage = !!headers['accept-language'];
153
- const hasSecFetchDest = !!headers['sec-fetch-dest'];
153
+ const hasSecFetchDest = !!headers['sec-fetch-dest'];
154
154
  const hasUpgradeInsecure = !!headers['upgrade-insecure-requests'];
155
155
 
156
- // If it has typical browser headers, we allow it.
157
156
  if (hasAcceptLanguage && (hasSecFetchDest || hasUpgradeInsecure)) {
158
157
  return true;
159
158
  }
160
159
 
161
- // If it has no browser headers and no trusted proxy headers -> It's likely a script.
162
160
  return false;
163
161
  }
164
162
 
@@ -171,9 +169,9 @@ export class AAMPPublisher {
171
169
  const sigHeader = reqHeaders[HEADERS.SIGNATURE]!;
172
170
  const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY]!;
173
171
 
174
- const headerJson = atob(payloadHeader);
172
+ const headerJson = atob(payloadHeader);
175
173
  const requestHeader = JSON.parse(headerJson);
176
-
174
+
177
175
  const signedRequest: SignedAccessRequest = {
178
176
  header: requestHeader,
179
177
  signature: sigHeader,
@@ -188,10 +186,20 @@ export class AAMPPublisher {
188
186
  ["verify"]
189
187
  );
190
188
 
189
+ const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
190
+ const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
191
+
191
192
  // Verify Core Logic
192
- const result = await this.verifyRequestLogic(signedRequest, agentKey, headerJson);
193
+ const result = await this.verifyRequestLogic(signedRequest, agentKey, proofToken, paymentCredential, headerJson);
193
194
 
194
195
  if (!result.allowed) {
196
+ console.log(`ā›” [AAMP BLOCK]
197
+ Agent: ${requestHeader.agent_id}
198
+ Reason: ${result.reason}
199
+ VisitorType: ${result.visitorType}
200
+ Proof: ${result.proofUsed || 'None'}
201
+ Identity Verified: ${result.identityVerified}`);
202
+
195
203
  return {
196
204
  allowed: false,
197
205
  status: 403,
@@ -200,6 +208,12 @@ export class AAMPPublisher {
200
208
  };
201
209
  }
202
210
 
211
+ console.log(`āœ… [AAMP ALLOW]
212
+ Agent: ${requestHeader.agent_id}
213
+ Reason: AAMP_VERIFIED
214
+ Payment Method: ${result.proofUsed}
215
+ Identity Verified: ${result.identityVerified}`);
216
+
203
217
  return {
204
218
  allowed: true,
205
219
  status: 200,
@@ -209,16 +223,19 @@ export class AAMPPublisher {
209
223
  };
210
224
 
211
225
  } catch (e) {
226
+ console.error(e);
212
227
  return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
213
228
  }
214
229
  }
215
230
 
216
231
  private async verifyRequestLogic(
217
- request: SignedAccessRequest,
232
+ request: SignedAccessRequest,
218
233
  requestPublicKey: CryptoKey,
234
+ proofToken?: string,
235
+ paymentCredential?: string,
219
236
  rawPayload?: string
220
237
  ): Promise<VerificationResult> {
221
-
238
+
222
239
  // 1. Replay Attack Prevention
223
240
  const requestTime = new Date(request.header.ts).getTime();
224
241
  if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
@@ -234,11 +251,14 @@ export class AAMPPublisher {
234
251
  let identityVerified = false;
235
252
  const claimedDomain = request.header.agent_id;
236
253
  const pubKeyString = await exportPublicKey(requestPublicKey);
237
-
254
+
255
+ console.log(` šŸ†” [AAMP Identity] Verifying DNS Binding for: ${claimedDomain}`);
256
+
238
257
  // Check Cache First
239
258
  const cachedKey = await this.cache.get(claimedDomain);
240
-
259
+
241
260
  if (cachedKey === pubKeyString) {
261
+ console.log(" ⚔ [AAMP Cache] Identity found in cache.");
242
262
  identityVerified = true;
243
263
  } else if (this.isDomain(claimedDomain)) {
244
264
  // Cache Miss: Perform DNS Fetch
@@ -260,11 +280,53 @@ export class AAMPPublisher {
260
280
  return { allowed: false, reason: 'POLICY_DENIED: RAG not allowed.', identityVerified };
261
281
  }
262
282
 
263
- // 5. Policy Check: Economics
283
+ // 5. Policy Check: Economics (v1.2)
264
284
  if (this.policy.requiresPayment) {
265
- const isAdExempt = this.policy.allowAdSupportedAccess && request.header.context.ads_displayed;
266
- if (!isAdExempt) {
267
- return { allowed: false, reason: 'PAYMENT_REQUIRED: Content requires payment or ads.', identityVerified };
285
+ let paymentSatisfied = false;
286
+
287
+ // Method A: Flexible Payment Callback (DB / Custom Logic)
288
+ if (this.policy.monetization?.checkPayment) {
289
+ const isPaid = await this.policy.monetization.checkPayment(request.header.agent_id, request.header.purpose);
290
+ if (isPaid) {
291
+ console.log(` šŸ’° [AAMP Audit] Whitelist Check Passed via Callback.`);
292
+ return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'WHITELIST_CALLBACK' };
293
+ }
294
+ }
295
+
296
+ // Method B: Payment Credentials (Unified JWT)
297
+ if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
298
+ const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
299
+ console.log(` šŸ” [AAMP Audit] Verifying Payment Credential (Issuer: ${issuer})...`);
300
+
301
+ const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
302
+ if (isValidCredential) {
303
+ console.log(` āœ… [AAMP Audit] Credential Signature VALID.`);
304
+ return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
305
+ } else {
306
+ console.log(` āŒ [AAMP Audit] Credential Signature INVALID.`);
307
+ }
308
+ }
309
+
310
+ // Method C: Ad-Supported (Proof Verification)
311
+ if (!paymentSatisfied && this.policy.allowAdSupportedAccess && request.header.context.ads_displayed) {
312
+ if (proofToken && this.policy.monetization?.adNetwork) {
313
+ const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
314
+ console.log(` šŸ“ŗ [AAMP Audit] Verifying Ad Proof (Issuer: ${issuer})...`);
315
+
316
+ const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
317
+ if (isValidProof) {
318
+ console.log(` āœ… [AAMP Audit] Ad Proof Signature VALID.`);
319
+ return { allowed: true, reason: 'OK', identityVerified, proofUsed: 'AD_PROOF_JWT' };
320
+ } else {
321
+ console.log(` āŒ [AAMP Audit] Ad Proof Signature INVALID.`);
322
+ }
323
+ } else {
324
+ console.log(` āš ļø [AAMP Audit] Ad Proof MISSING.`);
325
+ }
326
+ }
327
+
328
+ if (!paymentSatisfied) {
329
+ return { allowed: false, reason: 'PAYMENT_REQUIRED: Whitelist, Credential, and Ad Proof checks ALL failed.', identityVerified, proofUsed: 'NONE', visitorType: 'VERIFIED_AGENT' };
268
330
  }
269
331
  }
270
332
 
@@ -276,18 +338,42 @@ export class AAMPPublisher {
276
338
  // Allow HTTP for localhost testing
277
339
  const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
278
340
  const url = `${protocol}://${domain}${WELL_KNOWN_AGENT_PATH}`;
279
-
341
+
342
+ console.log(` šŸŒ [AAMP DNS] Fetching Manifest: ${url} ...`);
343
+
280
344
  // In production, we need a short timeout to prevent hanging
281
345
  const controller = new AbortController();
282
346
  const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
283
-
347
+
284
348
  const response = await fetch(url, { signal: controller.signal });
285
349
  clearTimeout(timeoutId);
286
-
287
- if (!response.ok) return false;
350
+
351
+ if (!response.ok) {
352
+ console.log(` āŒ [AAMP DNS] Fetch Failed: ${response.status}`);
353
+ return false;
354
+ }
355
+
288
356
  const manifest = await response.json() as AgentIdentityManifest;
289
- return manifest.agent_id === domain && manifest.public_key === requestKeySpki;
290
- } catch { return false; }
357
+ console.log(` šŸ“„ [AAMP DNS] Manifest received. Agent ID: ${manifest.agent_id}`);
358
+
359
+ // CHECK 1: Does the manifest actually belong to the domain?
360
+ if (manifest.agent_id !== domain) {
361
+ console.log(` āŒ [AAMP DNS] Mismatch: Manifest ID ${manifest.agent_id} != Claimed ${domain}`);
362
+ return false;
363
+ }
364
+
365
+ // CHECK 2: Does the key match?
366
+ if (manifest.public_key !== requestKeySpki) {
367
+ console.log(` āŒ [AAMP DNS] Key Mismatch: DNS Key != Request Key`);
368
+ return false;
369
+ }
370
+
371
+ console.log(` āœ… [AAMP DNS] Identity Confirmed.`);
372
+ return true;
373
+ } catch (e: any) {
374
+ console.log(` āŒ [AAMP DNS] Error: ${e.message}`);
375
+ return false;
376
+ }
291
377
  }
292
378
 
293
379
  private isDomain(s: string): boolean {
@@ -304,4 +390,28 @@ export class AAMPPublisher {
304
390
  [HEADERS.PROVENANCE_SIG]: signature
305
391
  };
306
392
  }
393
+
394
+ /**
395
+ * Handling Quality Feedback (The "Dispute" Layer)
396
+ * This runs when an Agent sends 'x-aamp-feedback'.
397
+ */
398
+ private async handleFeedback(token: string, headers: Record<string, string | undefined>) {
399
+ // NOTE: In production, you would fetch the Agent's specific key.
400
+ // For now, we assume standard Discovery or a centralized Key Set (like adNetwork).
401
+ // Ideally, the SDK config should have a 'qualityOracle' key set.
402
+
403
+ // 1. We just Decode it to Log it (Verification is optional but recommended)
404
+ try {
405
+ const parts = token.split('.');
406
+ const payload = JSON.parse(atob(parts[1]));
407
+
408
+ console.log(`\nšŸ“¢ [AAMP QUALITY ALERT] Feedback Received from ${payload.agent_id}`);
409
+ console.log(` Reason: ${payload.reason} | Score: ${payload.quality_score}`);
410
+ console.log(` Resource: ${payload.url}`);
411
+ console.log(` (Signature available for dispute evidence)`);
412
+
413
+ } catch (e) {
414
+ console.log(` āš ļø [AAMP Warning] Malformed Feedback Token.`);
415
+ }
416
+ }
307
417
  }
package/src/types.ts CHANGED
@@ -43,14 +43,35 @@ export interface IdentityCache {
43
43
  set(key: string, value: string, ttlSeconds: number): Promise<void>;
44
44
  }
45
45
 
46
+ /**
47
+ * Optional Monetization (The Settlement Layer)
48
+ */
46
49
  /**
47
50
  * Optional Monetization (The Settlement Layer)
48
51
  */
49
52
  export interface MonetizationConfig {
50
- method: 'BROKER' | 'CRYPTO' | 'PRIVATE_TREATY';
51
- location: string;
53
+ // Method 1: Payments (Flexible Callback)
54
+ // Developers implement their own logic (Database check, CMS lookup, etc.)
55
+ // Returns TRUE if the agent is a paid subscriber for this specific purpose.
56
+ checkPayment?: (agentId: string, purpose: string) => boolean | Promise<boolean>;
57
+
58
+ // Method 2: Ads (Proof Verification)
59
+ // Configuration to verify tokens from your Ad Provider (e.g. Google)
60
+ adNetwork?: {
61
+ jwksUrl: string; // e.g. "https://www.googleapis.com/oauth2/v3/certs"
62
+ issuer: string; // e.g. "https://accounts.google.com"
63
+ };
64
+
65
+ // Method 3: Payment Credentials (Unified JWT)
66
+ // Verifies "x-aamp-credential" for Broker or Direct payments.
67
+ paymentConfig?: {
68
+ jwksUrl: string; // e.g. "https://my-site.com/.well-known/jwks.json"
69
+ issuer: string; // e.g. "my-site.com"
70
+ };
52
71
  }
53
72
 
73
+
74
+
54
75
  /**
55
76
  * Handling Non-AAMP Visitors
56
77
  *
@@ -65,15 +86,15 @@ export interface AccessPolicy {
65
86
  allowTraining: boolean;
66
87
  allowRAG: boolean;
67
88
  attributionRequired: boolean;
68
-
89
+
69
90
  // Economic Signals
70
- allowAdSupportedAccess: boolean;
71
- requiresPayment: boolean;
91
+ allowAdSupportedAccess: boolean;
92
+ requiresPayment: boolean;
72
93
  paymentPointer?: string;
73
94
 
74
95
  // Identity Strictness
75
- requireIdentityBinding?: boolean;
76
-
96
+ requireIdentityBinding?: boolean;
97
+
77
98
  // V1.1: Optional Settlement Info
78
99
  monetization?: MonetizationConfig;
79
100
  }
@@ -81,7 +102,7 @@ export interface AccessPolicy {
81
102
  export interface ProtocolHeader {
82
103
  v: '1.1';
83
104
  ts: string;
84
- agent_id: string;
105
+ agent_id: string;
85
106
  resource: string;
86
107
  purpose: AccessPurpose;
87
108
  context: {
@@ -98,11 +119,12 @@ export interface SignedAccessRequest {
98
119
  export interface FeedbackSignal {
99
120
  target_resource: string;
100
121
  agent_id: string;
101
- quality_score: number;
122
+ quality_score: number;
102
123
  flags: QualityFlag[];
103
124
  timestamp: string;
104
125
  }
105
126
 
127
+ // Result of the full evaluation pipeline
106
128
  // Result of the full evaluation pipeline
107
129
  export interface EvaluationResult {
108
130
  allowed: boolean;
@@ -110,4 +132,15 @@ export interface EvaluationResult {
110
132
  reason: string;
111
133
  visitorType: 'VERIFIED_AGENT' | 'LIKELY_HUMAN' | 'UNIDENTIFIED_BOT';
112
134
  metadata?: any;
135
+ payment_status?: 'PAID_SUBSCRIBER' | 'AD_FUNDED' | 'UNPAID';
136
+ proofUsed?: string;
137
+ }
138
+
139
+ // Signed Quality Feedback (The "Report Card")
140
+ export interface FeedbackSignalToken {
141
+ url: string; // The resource being flagged
142
+ agent_id: string; // Who is flagging it (e.g. "bot.openai.com")
143
+ quality_score: number; // 0.0 to 1.0
144
+ reason: string; // e.g. "SEO_SPAM", "HATE_SPEECH"
145
+ timestamp: number;
113
146
  }
@@ -3,10 +3,10 @@
3
3
  * Run using: npm test
4
4
  * Location: sdk/typescript/test/handshake.spec.ts
5
5
  */
6
- import { AAMPAgent } from '../src/agent';
7
- import { AAMPPublisher } from '../src/publisher';
8
- import { AccessPurpose } from '../src/types';
9
- import { HEADERS } from '../src/constants';
6
+ import { AAMPAgent } from '../src/agent.js';
7
+ import { AAMPPublisher } from '../src/publisher.js';
8
+ import { AccessPurpose } from '../src/types.js';
9
+ import { HEADERS } from '../src/constants.js';
10
10
 
11
11
  async function runTest() {
12
12
  console.log("--- STARTING AAMP HANDSHAKE TEST ---");
@@ -29,7 +29,7 @@ async function runTest() {
29
29
  // TEST CASE A: Requesting RAG without Ads (Should FAIL due to Payment Requirement)
30
30
  console.log("\n[TEST A] Requesting RAG (No Ads)...");
31
31
  const reqA = await agent.createAccessRequest('/doc/1', AccessPurpose.RAG_RETRIEVAL, { adsDisplayed: false });
32
-
32
+
33
33
  const payloadA = JSON.stringify(reqA.header);
34
34
  const headersA = {
35
35
  [HEADERS.PAYLOAD]: btoa(payloadA),
@@ -45,7 +45,7 @@ async function runTest() {
45
45
  // TEST CASE B: Requesting RAG WITH Ads (Should SUCCEED via Exemption)
46
46
  console.log("\n[TEST B] Requesting RAG (With Ads)...");
47
47
  const reqB = await agent.createAccessRequest('/doc/1', AccessPurpose.RAG_RETRIEVAL, { adsDisplayed: true });
48
-
48
+
49
49
  const payloadB = JSON.stringify(reqB.header);
50
50
  const headersB = {
51
51
  [HEADERS.PAYLOAD]: btoa(payloadB),
package/tsconfig.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2020",
4
- "module": "CommonJS",
5
- "lib": ["ES2020", "DOM"],
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": [
7
+ "ES2020",
8
+ "DOM"
9
+ ],
6
10
  "declaration": true,
7
11
  "outDir": "./dist",
8
12
  "rootDir": "./src",
@@ -11,5 +15,7 @@
11
15
  "skipLibCheck": true,
12
16
  "forceConsistentCasingInFileNames": true
13
17
  },
14
- "include": ["src/**/*"]
18
+ "include": [
19
+ "src/**/*"
20
+ ]
15
21
  }