glippy-mcp 0.2.0 → 0.3.0
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/package.json +1 -1
- package/src/geo-checker.js +223 -9
package/package.json
CHANGED
package/src/geo-checker.js
CHANGED
|
@@ -3006,6 +3006,10 @@ function checkPerformance($) {
|
|
|
3006
3006
|
// CHECK CATEGORY 10: Agent Interactivity (WebMCP + UCP)
|
|
3007
3007
|
// ---------------------------------------------------------------------------
|
|
3008
3008
|
|
|
3009
|
+
// LATEST_UCP_VERSION: gating threshold for 2026-04-08 spec additions
|
|
3010
|
+
// (signing_keys, order webhook_url, etc. become required at this version).
|
|
3011
|
+
const LATEST_UCP_VERSION = '2026-04-08';
|
|
3012
|
+
|
|
3009
3013
|
function checkWebMCP($, pageType, ucpData) {
|
|
3010
3014
|
const checks = [];
|
|
3011
3015
|
let score = 0;
|
|
@@ -3350,6 +3354,40 @@ function checkWebMCP($, pageType, ucpData) {
|
|
|
3350
3354
|
const capabilities = capsArray; // Already normalized above
|
|
3351
3355
|
const transportKeys = ['rest', 'mcp', 'a2a', 'embedded'];
|
|
3352
3356
|
|
|
3357
|
+
// UCP CHECK 2.5: Cache Headers (only when caller passed response headers)
|
|
3358
|
+
const ucpHeaders = ucpData && ucpData.headers ? ucpData.headers : null;
|
|
3359
|
+
if (ucpHeaders) {
|
|
3360
|
+
const headerLookup = (n) => {
|
|
3361
|
+
const lower = n.toLowerCase();
|
|
3362
|
+
for (const k of Object.keys(ucpHeaders)) {
|
|
3363
|
+
if (k.toLowerCase() === lower) return String(ucpHeaders[k] || '');
|
|
3364
|
+
}
|
|
3365
|
+
return '';
|
|
3366
|
+
};
|
|
3367
|
+
const ct = headerLookup('content-type').toLowerCase();
|
|
3368
|
+
const cc = headerLookup('cache-control').toLowerCase();
|
|
3369
|
+
const ctOk = ct.startsWith('application/json');
|
|
3370
|
+
const ccTokens = cc.split(',').map(s => s.trim());
|
|
3371
|
+
const hasPublic = ccTokens.includes('public');
|
|
3372
|
+
const hasBadDirective = ccTokens.some(t => t === 'private' || t === 'no-store' || t === 'no-cache');
|
|
3373
|
+
const maxAgeMatch = cc.match(/max-age=(\d+)/);
|
|
3374
|
+
const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : -1;
|
|
3375
|
+
const ccOk = hasPublic && !hasBadDirective && maxAge >= 60;
|
|
3376
|
+
maxScore += 5;
|
|
3377
|
+
if (ctOk && ccOk) {
|
|
3378
|
+
score += 5;
|
|
3379
|
+
checks.push({ status: 'pass', label: 'UCP profile cache headers OK', detail: `Content-Type application/json with Cache-Control: public, max-age=${maxAge}` });
|
|
3380
|
+
} else {
|
|
3381
|
+
score += 2;
|
|
3382
|
+
const issues = [];
|
|
3383
|
+
if (!ctOk) issues.push(`content-type "${ct || 'missing'}" (expected application/json)`);
|
|
3384
|
+
if (!hasPublic) issues.push('cache-control missing "public"');
|
|
3385
|
+
if (hasBadDirective) issues.push('cache-control contains private/no-store/no-cache');
|
|
3386
|
+
if (maxAge < 60) issues.push(`max-age=${maxAge >= 0 ? maxAge : 'missing'} (expected >=60)`);
|
|
3387
|
+
checks.push({ status: 'warn', label: 'UCP profile cache headers need attention', detail: issues.slice(0, 3).join('; '), found: issues });
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3353
3391
|
// UCP CHECK 2: Profile Completeness
|
|
3354
3392
|
let completenessIssues = [];
|
|
3355
3393
|
if (!versionDatePattern.test(version)) completenessIssues.push('version not date-formatted (expected YYYY-MM-DD)');
|
|
@@ -3379,25 +3417,94 @@ function checkWebMCP($, pageType, ucpData) {
|
|
|
3379
3417
|
checks.push({ status: 'warn', label: `UCP profile has ${completenessIssues.length} issue(s)`, detail: completenessIssues.slice(0, 3).join('; '), found: completenessIssues.slice(0, 5) });
|
|
3380
3418
|
}
|
|
3381
3419
|
|
|
3382
|
-
// UCP CHECK 3: Capability Coverage
|
|
3420
|
+
// UCP CHECK 3: Capability Coverage (synced with extension processUCPProfile)
|
|
3383
3421
|
const capNames = capabilities.map(c => c.name || '');
|
|
3384
3422
|
const coreCapabilities = {
|
|
3385
3423
|
'dev.ucp.shopping.checkout': 'Checkout',
|
|
3386
3424
|
'dev.ucp.shopping.identity_linking': 'Identity Linking',
|
|
3387
3425
|
'dev.ucp.shopping.order': 'Order Management',
|
|
3426
|
+
'dev.ucp.shopping.cart': 'Cart',
|
|
3388
3427
|
};
|
|
3428
|
+
// Catalog has sub-capabilities; credit if any match the catalog prefix.
|
|
3429
|
+
const hasCatalog = capNames.some(n => n.startsWith('dev.ucp.shopping.catalog'));
|
|
3389
3430
|
const presentCore = Object.keys(coreCapabilities).filter(c => capNames.includes(c));
|
|
3390
|
-
|
|
3431
|
+
if (hasCatalog) presentCore.push('dev.ucp.shopping.catalog');
|
|
3432
|
+
const missingEntries = Object.entries(coreCapabilities).filter(([k]) => !capNames.includes(k));
|
|
3433
|
+
if (!hasCatalog) missingEntries.push(['dev.ucp.shopping.catalog', 'Catalog']);
|
|
3434
|
+
const missingCore = missingEntries.map(([, v]) => v);
|
|
3435
|
+
const totalCore = Object.keys(coreCapabilities).length + 1; // +1 for Catalog
|
|
3391
3436
|
|
|
3392
3437
|
maxScore += 10;
|
|
3393
|
-
if (presentCore.length ===
|
|
3438
|
+
if (presentCore.length === totalCore) {
|
|
3394
3439
|
score += 10;
|
|
3395
|
-
checks.push({ status: 'pass', label:
|
|
3440
|
+
checks.push({ status: 'pass', label: `All ${totalCore} core UCP capabilities declared`, detail: 'Checkout, Identity Linking, Order Management, Cart, and Catalog' });
|
|
3396
3441
|
} else if (presentCore.length > 0) {
|
|
3397
3442
|
score += 5;
|
|
3398
|
-
checks.push({ status: 'warn', label: `${presentCore.length}
|
|
3443
|
+
checks.push({ status: 'warn', label: `${presentCore.length}/${totalCore} core UCP capabilities declared`, detail: `Missing: ${missingCore.join(', ')}`, found: presentCore });
|
|
3444
|
+
} else {
|
|
3445
|
+
checks.push({ status: 'info', label: 'No core UCP capabilities declared', detail: 'Consider adding checkout, identity_linking, order, cart, and catalog capabilities' });
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
// 2026-04-08 spec gating: declared version >= 2026-04-08?
|
|
3449
|
+
const isV2 = versionDatePattern.test(version) && version >= LATEST_UCP_VERSION;
|
|
3450
|
+
|
|
3451
|
+
// UCP CHECK 3.5: Signing Keys (RFC 9421 ES256, mandatory in 2026-04-08)
|
|
3452
|
+
const signingKeys = Array.isArray(profile.signing_keys) ? profile.signing_keys : null;
|
|
3453
|
+
if (signingKeys && signingKeys.length > 0) {
|
|
3454
|
+
const malformed = signingKeys.filter(k => !k || !k.kid || k.kty !== 'EC' || k.crv !== 'P-256' || !k.x || !k.y);
|
|
3455
|
+
maxScore += 10;
|
|
3456
|
+
if (malformed.length === 0) {
|
|
3457
|
+
score += 10;
|
|
3458
|
+
checks.push({ status: 'pass', label: `${signingKeys.length} UCP signing key(s) declared`, detail: 'Profile advertises EC P-256 JWK(s) for RFC 9421 message signing' });
|
|
3459
|
+
} else {
|
|
3460
|
+
score += 3;
|
|
3461
|
+
checks.push({ status: 'warn', label: `${malformed.length}/${signingKeys.length} UCP signing key(s) malformed`, detail: 'Each key must have kid, kty=EC, crv=P-256, x, y', found: malformed.map(k => k && k.kid ? k.kid : '<missing kid>').slice(0, 5) });
|
|
3462
|
+
}
|
|
3463
|
+
} else if (isV2) {
|
|
3464
|
+
maxScore += 10;
|
|
3465
|
+
score += 3;
|
|
3466
|
+
checks.push({ status: 'warn', label: 'UCP signing keys missing', detail: 'UCP 2026-04-08 mandates RFC 9421 ES256 signatures; profile must publish signing_keys[]' });
|
|
3399
3467
|
} else {
|
|
3400
|
-
checks.push({ status: 'info', label: '
|
|
3468
|
+
checks.push({ status: 'info', label: 'UCP signing keys not declared', detail: 'UCP 2026-04-08 will require signing_keys[]; consider adding for forward compatibility' });
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
// UCP CHECK 3.6: Catalog Sub-Capability Coverage
|
|
3472
|
+
const catalogCaps = capabilities.filter(c => (c.name || '').startsWith('dev.ucp.shopping.catalog'));
|
|
3473
|
+
if (catalogCaps.length > 0) {
|
|
3474
|
+
const subs = ['search', 'lookup', 'get_product'];
|
|
3475
|
+
const presentSubs = subs.filter(s => catalogCaps.some(c => c.name === `dev.ucp.shopping.catalog.${s}` || c.name === 'dev.ucp.shopping.catalog'));
|
|
3476
|
+
const missingSubs = subs.filter(s => !presentSubs.includes(s));
|
|
3477
|
+
maxScore += 5;
|
|
3478
|
+
if (missingSubs.length === 0) {
|
|
3479
|
+
score += 5;
|
|
3480
|
+
checks.push({ status: 'pass', label: 'Catalog capability fully declared', detail: 'search, lookup, and get_product sub-capabilities all present' });
|
|
3481
|
+
} else {
|
|
3482
|
+
score += 3;
|
|
3483
|
+
checks.push({ status: 'warn', label: `Catalog declared with ${presentSubs.length}/${subs.length} sub-capabilities`, detail: `Missing: ${missingSubs.join(', ')}`, found: missingSubs });
|
|
3484
|
+
}
|
|
3485
|
+
} else if (capNames.includes('dev.ucp.shopping.cart')) {
|
|
3486
|
+
checks.push({ status: 'info', label: 'Cart declared without Catalog', detail: 'Consider adding catalog.search / catalog.lookup so agents can discover products before adding to cart' });
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
// UCP CHECK 3.7: Order Webhook URL (required in 2026-04-08)
|
|
3490
|
+
const orderCap = capabilities.find(c => c.name === 'dev.ucp.shopping.order');
|
|
3491
|
+
if (orderCap) {
|
|
3492
|
+
const webhookUrl = orderCap.config && orderCap.config.webhook_url;
|
|
3493
|
+
if (webhookUrl && typeof webhookUrl === 'string' && webhookUrl.startsWith('https://')) {
|
|
3494
|
+
maxScore += 5;
|
|
3495
|
+
score += 5;
|
|
3496
|
+
checks.push({ status: 'pass', label: 'Order webhook URL declared', detail: 'config.webhook_url is HTTPS, enabling real-time order updates' });
|
|
3497
|
+
} else if (webhookUrl) {
|
|
3498
|
+
maxScore += 5;
|
|
3499
|
+
score += 2;
|
|
3500
|
+
checks.push({ status: 'warn', label: 'Order webhook URL is not HTTPS', detail: 'config.webhook_url must use https://' });
|
|
3501
|
+
} else if (isV2) {
|
|
3502
|
+
maxScore += 5;
|
|
3503
|
+
score += 2;
|
|
3504
|
+
checks.push({ status: 'warn', label: 'Order webhook URL missing', detail: 'UCP 2026-04-08 requires config.webhook_url on the order capability for real-time updates' });
|
|
3505
|
+
} else {
|
|
3506
|
+
checks.push({ status: 'info', label: 'Order webhook URL not declared', detail: 'UCP 2026-04-08 will require config.webhook_url on order capabilities' });
|
|
3507
|
+
}
|
|
3401
3508
|
}
|
|
3402
3509
|
|
|
3403
3510
|
// UCP CHECK 4: Extension Support
|
|
@@ -3424,6 +3531,20 @@ function checkWebMCP($, pageType, ucpData) {
|
|
|
3424
3531
|
}
|
|
3425
3532
|
}
|
|
3426
3533
|
|
|
3534
|
+
// Cart-specific transport recommendation (2026-04-08 adds embedded binding).
|
|
3535
|
+
if (capNames.includes('dev.ucp.shopping.cart')) {
|
|
3536
|
+
const cartTransports = new Set(allTransports.map(t => t.transport));
|
|
3537
|
+
const cartRecommended = cartTransports.has('embedded') || cartTransports.has('mcp');
|
|
3538
|
+
maxScore += 3;
|
|
3539
|
+
if (cartRecommended) {
|
|
3540
|
+
score += 3;
|
|
3541
|
+
checks.push({ status: 'pass', label: 'Cart capability has embedded or MCP transport', detail: 'UCP 2026-04-08 recommends embedded or MCP transport for cart capability' });
|
|
3542
|
+
} else {
|
|
3543
|
+
score += 1;
|
|
3544
|
+
checks.push({ status: 'warn', label: 'Cart capability missing embedded/MCP transport', detail: 'UCP 2026-04-08 recommends adding an embedded transport binding for cart so agents can hand off to checkout' });
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3427
3548
|
if (allTransports.length > 1) {
|
|
3428
3549
|
maxScore += 10;
|
|
3429
3550
|
const httpsTransports = allTransports.filter(t => (t.endpoint || '').startsWith('https://') && (t.schema || '').startsWith('https://'));
|
|
@@ -3470,8 +3591,8 @@ function checkWebMCP($, pageType, ucpData) {
|
|
|
3470
3591
|
// UCP CHECK 8: Page-Type-Specific Recommendations
|
|
3471
3592
|
const commercePageTypes = ['product', 'ecommerce', 'saas', 'local-business'];
|
|
3472
3593
|
const ucpRecommendations = {
|
|
3473
|
-
'product': 'Should have checkout +
|
|
3474
|
-
'ecommerce': 'Should have checkout +
|
|
3594
|
+
'product': 'Should have checkout + cart + catalog + fulfillment capabilities',
|
|
3595
|
+
'ecommerce': 'Should have checkout + cart + catalog; consider identity linking for personalization',
|
|
3475
3596
|
'saas': 'Should have checkout for subscription/trial flows',
|
|
3476
3597
|
'local-business': 'Consider checkout for booking/purchasing services',
|
|
3477
3598
|
'homepage': 'UCP profile should be accessible at domain root /.well-known/ucp',
|
|
@@ -3480,6 +3601,51 @@ function checkWebMCP($, pageType, ucpData) {
|
|
|
3480
3601
|
if (ucpRecommendations[pageType]) {
|
|
3481
3602
|
checks.push({ status: 'pass', label: `UCP detected on ${pageType} page`, detail: ucpRecommendations[pageType] });
|
|
3482
3603
|
}
|
|
3604
|
+
|
|
3605
|
+
// UCP CHECK 9: Disclosure / Eligibility / Signals / Delegation feature advertisement (info-only).
|
|
3606
|
+
const advertisedFeatures = new Set();
|
|
3607
|
+
for (const cap of capabilities) {
|
|
3608
|
+
const feats = Array.isArray(cap.features) ? cap.features : [];
|
|
3609
|
+
for (const f of feats) {
|
|
3610
|
+
if (typeof f === 'string') advertisedFeatures.add(f);
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
const trackedFeatures = ['eligibility_claims', 'signals', 'disclosure_messages', 'link_delegation'];
|
|
3614
|
+
const presentFeats = trackedFeatures.filter(f => advertisedFeatures.has(f));
|
|
3615
|
+
if (presentFeats.length > 0) {
|
|
3616
|
+
checks.push({ status: 'info', label: `${presentFeats.length} UCP feature(s) advertised`, detail: `Capabilities advertise: ${presentFeats.join(', ')}`, found: presentFeats });
|
|
3617
|
+
} else {
|
|
3618
|
+
checks.push({ status: 'info', label: 'No optional UCP features advertised', detail: 'eligibility_claims, signals, disclosure_messages, and link_delegation are optional but improve agent trust negotiation' });
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
// UCP CHECK 10: Spec Version Currency (info-only).
|
|
3622
|
+
if (versionDatePattern.test(version)) {
|
|
3623
|
+
if (version === LATEST_UCP_VERSION) {
|
|
3624
|
+
checks.push({ status: 'info', label: 'UCP version is current', detail: `Profile declares the latest known UCP version (${version})` });
|
|
3625
|
+
} else if (version < LATEST_UCP_VERSION) {
|
|
3626
|
+
checks.push({ status: 'info', label: 'UCP version is older than latest', detail: `Profile declares ${version}; latest known is ${LATEST_UCP_VERSION}` });
|
|
3627
|
+
} else {
|
|
3628
|
+
checks.push({ status: 'info', label: 'UCP version newer than checker knows', detail: `Profile declares ${version}; this checker is calibrated against ${LATEST_UCP_VERSION}` });
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
// UCP CHECK 11: A2A agent-card.json (only when profile advertises an a2a transport)
|
|
3633
|
+
const agentCard = ucpData && ucpData.agentCard;
|
|
3634
|
+
if (agentCard) {
|
|
3635
|
+
if (agentCard.exists && agentCard.valid) {
|
|
3636
|
+
maxScore += 3;
|
|
3637
|
+
score += 3;
|
|
3638
|
+
checks.push({ status: 'pass', label: 'A2A agent card found', detail: '/.well-known/agent-card.json is reachable and parses as JSON' });
|
|
3639
|
+
} else if (agentCard.exists && !agentCard.valid) {
|
|
3640
|
+
maxScore += 3;
|
|
3641
|
+
checks.push({ status: 'warn', label: 'A2A agent card not valid JSON', detail: '/.well-known/agent-card.json was reachable but did not parse as JSON' });
|
|
3642
|
+
} else if (agentCard.missing) {
|
|
3643
|
+
maxScore += 3;
|
|
3644
|
+
checks.push({ status: 'warn', label: 'A2A agent card not found', detail: 'Profile advertises an a2a transport but /.well-known/agent-card.json returns 404' });
|
|
3645
|
+
} else {
|
|
3646
|
+
checks.push({ status: 'info', label: 'A2A agent card unreachable', detail: agentCard.statusCode ? `HTTP ${agentCard.statusCode}` : 'fetch error' });
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3483
3649
|
}
|
|
3484
3650
|
} else {
|
|
3485
3651
|
// No UCP profile found — informational only, no penalty
|
|
@@ -3487,10 +3653,15 @@ function checkWebMCP($, pageType, ucpData) {
|
|
|
3487
3653
|
if (commercePageTypes.includes(pageType)) {
|
|
3488
3654
|
checks.push({ status: 'info', label: 'No UCP discovery file found', detail: 'UCP enables AI agents to discover commerce capabilities via /.well-known/ucp' });
|
|
3489
3655
|
} else {
|
|
3490
|
-
checks.push({ status: 'info', label: 'No UCP discovery file found', detail: 'UCP enables AI agents to discover commerce capabilities
|
|
3656
|
+
checks.push({ status: 'info', label: 'No UCP discovery file found', detail: 'UCP enables AI agents to discover commerce capabilities - most relevant for commerce pages' });
|
|
3491
3657
|
}
|
|
3492
3658
|
}
|
|
3493
3659
|
|
|
3660
|
+
// Shopify dual-surface info shortcut (fires whether or not UCP profile exists).
|
|
3661
|
+
if (ucpData && ucpData.shopifyHosted) {
|
|
3662
|
+
checks.push({ status: 'info', label: 'Shopify-hosted: dual UCP surface expected', detail: 'Per-shop endpoint at /api/ucp/mcp; global catalog at https://discover.shopifyapps.com/global/mcp' });
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3494
3665
|
return { checks, score: maxScore > 0 ? Math.round((score / maxScore) * 100) : 0, category: 'Agent Interactivity' };
|
|
3495
3666
|
}
|
|
3496
3667
|
|
|
@@ -4766,11 +4937,54 @@ async function checkGEO(domain, options = {}) {
|
|
|
4766
4937
|
const profile = JSON.parse(ucpRes.body);
|
|
4767
4938
|
output.ucpProfile.exists = true;
|
|
4768
4939
|
output.ucpProfile.content = profile;
|
|
4940
|
+
output.ucpProfile.headers = ucpRes.headers || {};
|
|
4769
4941
|
}
|
|
4770
4942
|
} catch (err) {
|
|
4771
4943
|
output.ucpProfile.error = err.message;
|
|
4772
4944
|
}
|
|
4773
4945
|
|
|
4946
|
+
// --- /.well-known/agent-card.json (A2A discovery; only meaningful when profile advertises a2a) ---
|
|
4947
|
+
try {
|
|
4948
|
+
const services = output.ucpProfile.content && output.ucpProfile.content.ucp && output.ucpProfile.content.ucp.services;
|
|
4949
|
+
const advertisesA2a = services && Object.values(services).some(svc => svc && typeof svc === 'object' && svc.a2a);
|
|
4950
|
+
if (advertisesA2a) {
|
|
4951
|
+
const cardUrl = `${baseUrl}/.well-known/agent-card.json`;
|
|
4952
|
+
const cardRes = await throttledFetchUrl(cardUrl, FETCH_TIMEOUT_MS, MAX_TEXT_BODY_SIZE).catch(() => ({ body: null, statusCode: null }));
|
|
4953
|
+
if (cardRes.statusCode === 200 && cardRes.body) {
|
|
4954
|
+
try {
|
|
4955
|
+
JSON.parse(cardRes.body);
|
|
4956
|
+
output.ucpProfile.agentCard = { url: cardUrl, exists: true, valid: true };
|
|
4957
|
+
} catch {
|
|
4958
|
+
output.ucpProfile.agentCard = { url: cardUrl, exists: true, valid: false };
|
|
4959
|
+
}
|
|
4960
|
+
} else if (cardRes.statusCode === 404) {
|
|
4961
|
+
output.ucpProfile.agentCard = { url: cardUrl, exists: false, missing: true };
|
|
4962
|
+
} else {
|
|
4963
|
+
output.ucpProfile.agentCard = { url: cardUrl, exists: false, statusCode: cardRes.statusCode };
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
} catch (err) {
|
|
4967
|
+
output.ucpProfile.agentCard = { error: err.message };
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4970
|
+
// --- Shopify host detection (for dual-surface info shortcut in checks) ---
|
|
4971
|
+
try {
|
|
4972
|
+
const homepageHeaders = homepageRes && homepageRes.headers ? homepageRes.headers : {};
|
|
4973
|
+
const headerLookup = (n) => {
|
|
4974
|
+
const lower = n.toLowerCase();
|
|
4975
|
+
for (const k of Object.keys(homepageHeaders)) {
|
|
4976
|
+
if (k.toLowerCase() === lower) return String(homepageHeaders[k] || '');
|
|
4977
|
+
}
|
|
4978
|
+
return '';
|
|
4979
|
+
};
|
|
4980
|
+
const host = (cleanDomain || '').toLowerCase();
|
|
4981
|
+
const isShopifyDomain = host.endsWith('.myshopify.com') || host === 'myshopify.com';
|
|
4982
|
+
const isShopifyByHeader = !!(headerLookup('x-shopid') || headerLookup('x-shardid') || headerLookup('x-shopify-stage') || headerLookup('powered-by').toLowerCase().includes('shopify'));
|
|
4983
|
+
output.ucpProfile.shopifyHosted = isShopifyDomain || isShopifyByHeader;
|
|
4984
|
+
} catch (err) {
|
|
4985
|
+
output.ucpProfile.shopifyHosted = false;
|
|
4986
|
+
}
|
|
4987
|
+
|
|
4774
4988
|
// --- Homepage (full 16-category analysis) ---
|
|
4775
4989
|
try {
|
|
4776
4990
|
output.homepage.statusCode = homepageRes.statusCode;
|