← back to Ticket System
data/claude-run-11175/evidence/TK-11175-20260910T143730Z/tk11175-api-liveness-probe.mjs
42 lines
// TK-11175 READ-ONLY liveness probe. GET-only. No writes, no spend, no mutation.
// Answers: is Content API for Shopping v2.1 still serving on 2026-09-10, and is Merchant API v1 serving?
import fs from 'fs'; import crypto from 'crypto';
const HOME = process.env.HOME;
const SA_PATH = HOME + '/Projects/secrets-manager/gmc-sa-146735262.json';
const MERCHANT = '146735262';
const b64 = b => Buffer.from(b).toString('base64').replace(/=/g,'').replace(/\+/g,'-').replace(/\//g,'_');
async function mcToken(){
const SA = JSON.parse(fs.readFileSync(SA_PATH,'utf8'));
const now = Math.floor(Date.now()/1000);
const si = b64(JSON.stringify({alg:'RS256',typ:'JWT'}))+'.'+b64(JSON.stringify({iss:SA.client_email,scope:'https://www.googleapis.com/auth/content',aud:'https://oauth2.googleapis.com/token',iat:now,exp:now+3600}));
const s = crypto.createSign('RSA-SHA256'); s.update(si); const jwt = si+'.'+b64(s.sign(SA.private_key));
const r = await fetch('https://oauth2.googleapis.com/token',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({grant_type:'urn:ietf:params:oauth:grant-type:jwt-bearer',assertion:jwt})});
const j = await r.json(); if(!j.access_token) throw new Error('token fail: '+JSON.stringify(j).slice(0,200)); return j.access_token;
}
const t = await mcToken(); const H = { Authorization: 'Bearer '+t };
const probes = [
['CONTENT_v2.1 productstatuses', `https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/productstatuses?maxResults=1`],
['CONTENT_v2.1 products', `https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/products?maxResults=1`],
['MERCHANT_v1 products', `https://merchantapi.googleapis.com/products/v1/accounts/${MERCHANT}/products?pageSize=1`],
['MERCHANT_v1 accounts/issues', `https://merchantapi.googleapis.com/accounts/v1/accounts/${MERCHANT}/issues`],
];
const out = [];
for (const [name,url] of probes){
try {
const r = await fetch(url,{headers:H});
const body = await r.text();
let shape = 'empty';
try { const j = JSON.parse(body);
if (j.error) shape = 'ERROR: '+(j.error.status||'')+' '+String(j.error.message||'').slice(0,110);
else if (j.resources) shape = `OK resources=${j.resources.length}`;
else if (j.productStatuses) shape = `OK productStatuses=${j.productStatuses.length}`;
else if (j.products) shape = `OK products=${j.products.length}`;
else if (j.accountIssues) shape = `OK accountIssues=${(j.accountIssues||[]).length}`;
else shape = 'OK keys=['+Object.keys(j).join(',')+']';
} catch { shape = 'non-JSON len='+body.length; }
out.push({name, http:r.status, shape});
} catch(e){ out.push({name, http:'NETERR', shape:String(e.message).slice(0,110)}); }
}
console.log('probe_at=' + new Date().toISOString());
for (const o of out) console.log(String(o.http).padEnd(7), o.name.padEnd(32), o.shape);