Setting the file. One moment.
Infer · Browser To API · browserbase/skills · Skills Docs
ContentsBack to the top of the page scripts/infer.mjs
scripts/ infer.mjs
JavaScript · 142 lines · 5 KB
from
'./lib/schema-merge.mjs'
;
12 import { makeRedactor } from './lib/redact.mjs' ;
13
14 function pathHash ( method , p ) {
15 return crypto. createHash ( 'sha1' ). update ( `${ method } ${ p }` ). digest ( 'hex' ). slice ( 0 , 10 );
16 }
17
18 function inferAuthHeaders ( samples ) {
19 const seen = new Set ();
20 for ( const s of samples) {
21 for ( const k of Object. keys (s.reqHeaders || {})) {
22 const lk = k. toLowerCase ();
23 if (lk === 'authorization' || lk === 'x-api-key' || /token/ . test (lk) || / ^ x- . * -auth/ . test (lk)) {
24 seen. add (lk);
25 }
26 }
27 }
28 return [ ... seen]. sort ();
29 }
30
31 export function infer ( outDir , opts = {}) {
32 const redactor = makeRedactor ({ extra: opts.redact || [] });
33
34 const endpoints = readJsonl ( intermediatePath (outDir, 'endpoints.jsonl' ));
35 const samplesByKey = new Map ();
36 for ( const row of readJsonl ( intermediatePath (outDir, 'endpoint-samples.jsonl' ))) {
37 samplesByKey. set (row.endpointKey, row.samples);
38 }
39
40 ensureDir (path. join (outDir, 'samples' ));
41 const enriched = [];
42
43 for ( const ep of endpoints) {
44 const samples = samplesByKey. get (ep.endpointKey) || [];
45 const reqProto = newProto ();
46 const respProtoByStatus = new Map (); // status -> proto
47
48 let pickedReqExample = null ;
49 let pickedRespExample = null ;
50 let pickedReqStatus = null , pickedRespStatus = null ;
51
52 for ( const s of samples) {
53 if (s.reqBody != null && typeof s.reqBody === 'object' ) {
54 ingest (reqProto, s.reqBody);
55 if ( ! pickedReqExample) { pickedReqExample = s.reqBody; pickedReqStatus = s.status; }
56 }
57 if (s.respBody != null && typeof s.respBody === 'object' ) {
58 // Skip when we have no status: emit.mjs only renders schemas under
59 // statuses that appear in ep.statusCodes (which excludes nulls), so
60 // a body keyed under "0" would be silently discarded.
61 if (s.status == null ) continue ;
62 let p = respProtoByStatus. get (s.status);
63 if ( ! p) { p = newProto (); respProtoByStatus. set (s.status, p); }
64 ingest (p, s.respBody);
65 if (s.status >= 200 && s.status < 300 && ! pickedRespExample) {
66 pickedRespExample = s.respBody;
67 pickedRespStatus = s.status;
68 }
69 }
70 }
71
72 const requestBodyKnown = reqProto.samples > 0 ;
73 const responseBodyKnown = [ ... respProtoByStatus. values ()]. some ( p => p.samples > 0 );
74
75 const requestSchema = requestBodyKnown ? toSchema (reqProto) : null ;
76 const responseSchemas = {};
77 for ( const [ status , p ] of respProtoByStatus. entries ()) {
78 responseSchemas[ String (status)] = toSchema (p);
79 }
80
81 // Determine the canonical content-type per role from sample headers.
82 const reqCT = inferContentType (samples, 'reqHeaders' );
83 const respCTByStatus = {};
84 for ( const s of samples) {
85 const status = s.status ?? 0 ;
86 if ( ! respCTByStatus[status]) respCTByStatus[status] = inferContentType ([s], 'respHeaders' );
87 }
88
89 // Redact once and reuse for both the persisted sample file and the inline
90 // OpenAPI example. (Calling redactBody twice double-counts redactions.)
91 const ph = pathHash (ep.method, ep.path);
92 const reqExample = pickedReqExample != null ? redactor. redactBody (pickedReqExample) : null ;
93 const respExample = pickedRespExample != null ? redactor. redactBody (pickedRespExample) : null ;
94 const reqHeaders = redactor. redactHeaders (samples[ 0 ]?.reqHeaders || {});
95 const respHeaders = redactor. redactHeaders (samples[ 0 ]?.respHeaders || {});
96
97 const example = {
98 endpoint: ep.endpointKey,
99 request: { status: pickedReqStatus, headers: reqHeaders, body: reqExample },
100 response: { status: pickedRespStatus, headers: respHeaders, body: respExample },
101 };
102 writeJson ( samplePath (outDir, ep.method, ph), example);
103
104 enriched. push ({
105 ... ep,
106 pathHash: ph,
107 requestBodyKnown,
108 responseBodyKnown,
109 requestSchema,
110 responseSchemas,
111 requestContentType: reqCT,
112 responseContentTypes: respCTByStatus,
113 requestExample: reqExample,
114 responseExample: respExample,
115 observedAuthHeaders: inferAuthHeaders (samples),
116 });
117 }
118
119 writeJsonl ( intermediatePath (outDir, 'endpoints.with-schemas.jsonl' ), enriched);
120
121 // Also persist redaction stats for the report.
122 writeJson ( intermediatePath (outDir, 'redaction-stats.json' ), redactor.counts);
123
124 return { endpoints: enriched. length , redactor: redactor.counts };
125 }
126
127 function inferContentType ( samples , headerField ) {
128 for ( const s of samples) {
129 const headers = s[headerField] || {};
130 for ( const [ k , v ] of Object. entries (headers)) {
131 if (k. toLowerCase () === 'content-type' ) return String (v). split ( ';' )[ 0 ]. trim ();
132 }
133 }
134 return null ;
135 }
136
137 if ( import . meta .url === `file://${ process . argv [ 1 ] }` ) {
138 const out = process.argv[ 2 ];
139 if ( ! out) { console. error ( 'usage: infer.mjs <out-dir>' ); process. exit ( 2 ); }
140 const stats = infer (out);
141 console. log ( `infer: ${ stats . endpoints } endpoints (redactions: ${ stats . redactor . headers }h ${ stats . redactor . bodyKeys }k ${ stats . redactor . bodyValues }v)` );
142 }