AMPのキャッシュを消す with NodeJS

AMPのキャッシュを消すのが地味に面倒だったのでメモ
他の言語（PHP、Python）はあったけどNodeJSはなかったので実装した。

公式：https://developers.google.com/amp/cache/update-cache
参考１：https://beiznotes.org/amp-update-cache/
参考２：https://www.monotalk.xyz/blog/amp-%E3%81%AE-update-cache-%E8%A6%81%E6%B1%82%E3%82%92-apikeypub-%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E9%80%81%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B/

秘密鍵、公開鍵の作成が必要

```sh
# 公開鍵、秘密鍵を作成
$ openssl genrsa 2048 > private-key.pem
$ openssl rsa -in private-key.pem -pubout > public-key.pem
```

公開鍵(public-key.pem)は
`https://{ドメイン}/.well-known/amphtml/apikey.pub`
にリネームして格納、.well-known/amphtmlはフォルダがない場合は作る

apikey.pubはContent-Typeが`text/plain`形式でレスポンス返却するようにする
NginXの場合、`/etc/nginx/mime.types`にpubのファイル拡張子のdefaultのContent-Typeを追加

```/etc/nginx/mime.types
types {

     ...中略

     # 追加
     text/plain                                       pub; 
}
```

curlコマンドでtext/plainで返却されているか検証する

```
$ curl -I https://example.com/.well-known/amphtml/apikey.pub
```

`/var/www/amp-keys/private-key.pem`に秘密鍵をおいている前提、
ドメイン名は`example.com`という想定（適宜変更）

```amp-invalidate.js
const axios = require('axios')
const fs = require('fs')
const crypto = require('crypto')
const program = require('commander')


// 環境変数
const DOMAIN = 'example.com'
// 秘密鍵のパス
const privateKeyPath = '/var/www/amp-keys/private-key.pem'

// AMPホスティングCDNのドメインを取得
async function getUpdateCacheApiDomainSuffix() {
  const data = await axios.get('https://cdn.ampproject.org/caches.json').then(res => res.data)
  const updateCacheApiDomainSuffixes = []
  for (let cache of data.caches) {
    updateCacheApiDomainSuffixes.push(cache.updateCacheApiDomainSuffix)
  }
  return updateCacheApiDomainSuffixes
}

// AMPUrlフォーマット：https://developers.google.com/amp/cache/overview#amp-cache-url-format
function AMPUrlFormat(url) {
  return url.replace('-', '--').replace('.', '-')
}

async function main() {

  program
   .version('1.0.0')
   .option('-p, --ampPath <ampPath>', 'ampPath')
   .parse(process.argv)

  const ampPath = program.ampPath
  if (!ampPath) {
    console.log('-p /amp/page')
    return
  }

  const updateCacheApiDomainSuffixes = await getUpdateCacheApiDomainSuffix()
  const ts = Math.floor(new Date().getTime() / 1000) // Unix秒（リクエスト時の60秒内に収めないといけない）

  const privateKey = fs.readFileSync(privateKeyPath)

  for (let updateCacheApiDomainSuffix of updateCacheApiDomainSuffixes) {
    const AMP_BASE_URL = `https://${AMPUrlFormat(DOMAIN)}.${updateCacheApiDomainSuffix}`
    const SIGNATURE_URL = `/update-cache/c/s/${DOMAIN}${ampPath}?amp_action=flush&amp_ts=`

    // AMP CacheのURLのドメイン部を除去した文字列をダイジェスト値として、電子署名を生成
    const url = SIGNATURE_URL + ts
    const sign = crypto.createSign('RSA-SHA256')
    sign.update(url)
    // 生成した電子署名をbase64エンコードし、`amp_url_signature` として使う。
    const signature = sign.sign(privateKey, 'base64')

    // update-cache の要求URLを生成し、リクエスト送付
    const ampUrl = AMP_BASE_URL + url + '&amp_url_signature=' + signature
    console.log(ampUrl)
    const result = await axios.get(ampUrl).then(res => res.data).catch(res => res.data)
    // 成功するとOK、指定のCDNにない場合は404エラーになる
    console.log(result)
  }

}
main()
```

実行はつぎのように`-p`オプションにampページのパスを指定

```
$ node amp-invalidate.js -p /amp/page
```

実際はsitemap.xmlとかかまして一括でやる場合の方が多い