本文へ移動

部屋のサーバーのAPI ​

部屋のサーバー(Cloudflare版とVercel版)が受けるHTTPのAPIです。どちらの部屋のサーバーも、同じ経路に同じ形で答えます。部屋に入ったあとのWebSocketでのやりとりは、開発者向けの部屋のプロトコルにあります。

エンドポイントの一覧 ​

メソッドパス内容
POST/v1/rooms部屋を作る
GET/v1/rooms部屋の一覧
GET/v1/rooms/:roomKey部屋を鍵で1つ引く
DELETE/v1/rooms/:roomKey部屋を削除する(その部屋のGMだけ)
GET/v1/rooms/:roomKey/ws部屋に入る(WebSocketにつなぎ替える)
HEAD/v1/rooms/:roomKey/assets/:sha256部屋のアセットが置かれているか
GET/v1/rooms/:roomKey/assets/:sha256部屋のアセットの中身
PUT/v1/rooms/:roomKey/assets/:sha256部屋のアセットを置く(その部屋のGMとPLだけ)
DELETE/v1/rooms/:roomKey/assets/:sha256部屋のアセットを消す(その部屋のGMだけ)
GET/v1/rooms/:roomKey/assets部屋に置いてあるアセットの一覧
POST/v1/rooms/:roomKey/assets/:sha256/uploads置き場所へ直接上げる先を受け取る(Vercel版だけ)
POST/v1/rooms/:roomKey/assets/:sha256/uploads/complete直接上げ終えたことを知らせる(Vercel版だけ)
POST/v1/rooms/:roomKey/reports運営する人に通報する(部屋の席ならだれでも)
GET/v1/auth/configログインに使える提供元({"providers":["discord"]})
GET/v1/login/discordログインを始める(ページとして開く)
GET/v1/login/discord/callbackDiscordから戻ってくる(Discordのアプリに登録するURL)
POST/v1/auth/tokenログインのコードかリフレッシュトークンを、トークンの組に替える
POST/v1/auth/logoutログアウト。そのリフレッシュトークンの系統を無効にする
GET/v1/accountログインしている人
DELETE/v1/accountアカウントを消す
GET/v1/contact運営する人に断られたときの問い合わせ先({"contact":"https://…"}、無ければnull)
OPTIONS上のパスブラウザのプリフライトに答える
GET/admin運営する人の管理画面(→ 管理画面のAPI)

共通の決まり ​

  • ログインを始めるページ(/v1/login/discord)と、Discordから戻ってくるページ(/v1/login/discord/callback)のほかは、どれもACCESS_CONTROL_ALLOW_ORIGINに書いたOriginのページからしか受けません。ほかのOriginには403 origin-not-allowedを返します。この2つはブラウザがページとして開くもので、戻り先を許可したOriginに限ることで守ります。
  • 許可したOriginからのプリフライトには204で答え、Access-Control-Allow-HeadersでContent-Type、Authorization、X-Asset-Nameを許します。Access-Control-Max-Age: 86400(1日)を付けます。
  • :roomKeyは部屋の鍵で、小文字の16進で書いた32文字です。:sha256は、中身のSHA-256を小文字の16進で書いた64文字です。どちらかの形が違えば400 invalid-idを返します。
  • 運営する人がBANした送信元には、どれにも403 bannedを返します。ログインの2つのページは、Axeの戻り先にlogin_error=bannedを付けて戻します(→ 管理画面とBAN)。
  • 運営する人が緊急停止しているあいだは、止めたもの(部屋を作る、部屋のアセットを置く)に503 pausedを返します。新しいログインを止めているときは、Discordから戻ってくるページが、Axeの戻り先にlogin_error=pausedを付けて戻します(→ 緊急停止)。
  • ADMIN_CONTACT_URLを入れていると、403 bannedと503 pausedの本文に問い合わせ先を足します({"error":"banned","contact":"https://…"})。ログインの戻り先にも、login_error=bannedとlogin_error=pausedのときはlogin_contact=<問い合わせ先>を足します。GET /v1/contactは、BANした送信元からも読めます。
  • エラーの本文は{"error":"<コード>"}の形です(→ エラー)。
  • 設定が読めないときは、/v1/の下のすべてのリクエストに500 config-invalidを返します(→ 設定項目)。

部屋 ​

POST /v1/rooms ​

ログインした人だけが作れます。作った人が、その部屋のGMになります。

http
POST /v1/rooms
Origin: https://axe.example.com
Authorization: Bearer <アクセストークン>
Content-Type: application/json

{ "name": "金曜の卓", "password": "secret" }
フィールド型内容
name文字列部屋の名前。1〜255文字
password文字列省けば、パスワードの無い部屋。64文字まで

201で次を返します。

json
{ "roomKey": "511c18c897b08d168145a815870cf90b", "roomId": "O46" }
  • roomKey:部屋の鍵。128ビットの乱数を小文字の16進で書いた32文字。部屋に入るときと、招待リンクに使います。
  • roomId:表示用の3文字。ほかの部屋とぶつかることがあります。部屋はroomKeyで区別します。
状態本文起きるとき
400{"error":"invalid-request"}本文がJSONでない、名前やパスワードの形が違う
401{"error":"login-required"}アクセストークンが無いか、無効か、期限切れ
403{"error":"banned"}運営する人が、そのDiscordのアカウントか送信元をBANした
429{"error":"rate-limited"}送信元から部屋を作る・入る・削除する回数が上限を超えた
503{"error":"login-unavailable"}この部屋のサーバーにログインが置かれていない

GET /v1/rooms ​

json
{
  "rooms": [
    {
      "roomKey": "511c18c8…",
      "roomId": "O46",
      "name": "金曜の卓",
      "hasPassword": true,
      "members": 2
    }
  ]
}

新しく作られた順に、1000件まで返します。membersはいま入っている人数です。誰もいない部屋も、消えるまでは一覧に出ます。部屋のサーバーが落ちて、出ていく知らせが無いまま接続が切れたときは、見回りで数え直します(Cloudflare版は1時間ごと、Vercel版は30秒ごと)。

アクセストークンを付けて取ると、その人がGMの部屋に"deletable": trueが付きます。その部屋は、1000件に入らない古いものでも必ず返します。トークンが無いか読めないとき、GMでない部屋には付きません。

一覧は、だれが取っても部屋の鍵を返します(→ 部屋の鍵を知っていれば入れる)。送信元ごとの読み出しの上限(READ_LIMITER。部屋のアセットを読むのと合わせて1分600件)を超えると、429 rate-limitedを返します。

GET /v1/rooms/:roomKey ​

部屋の鍵で、1つの部屋を引きます。答えは{ "room": { … } }で、roomの形は一覧の1件と同じです。一覧に入らない古い部屋も引けるので、Axeは招待リンクを開いたときと、つなぎ直すときにこれを使います。アクセストークンを付ければ、その人がGMの部屋に"deletable": trueが付きます。部屋が無ければ404 room-not-found、鍵の形が違えば400 invalid-idを返します。数え方(READ_LIMITER)は一覧と同じです。

DELETE /v1/rooms/:roomKey ​

部屋を削除します。削除できるのは、その部屋のGMだけです。

http
DELETE /v1/rooms/511c18c897b08d168145a815870cf90b
Origin: https://axe.example.com
Authorization: Bearer <アクセストークン>

204を返します。部屋にいる人にはroom-deletedを送ってから、接続を閉じます。そのあとは保持期間が過ぎたときと同じく、部屋を一覧から外し、置いたものと部屋のアセットをすべて消します。消しているあいだ(一覧やR2を待つあいだ)は、新しい接続もhelloもroom-not-foundで断り、部屋のアセットのトークンも渡し直しません。保持期間が過ぎて消すときも同じです。

状態本文起きるとき
400{"error":"invalid-id"}部屋の鍵の形が違う
401{"error":"login-required"}アクセストークンが無いか、無効か、期限切れ
403{"error":"not-game-master"}その部屋のGMでない
404{"error":"room-not-found"}部屋が無い(作られていないか、消えた)
429{"error":"rate-limited"}送信元から部屋を作る・入る・削除する回数が上限を超えた
503{"error":"login-unavailable"}この部屋のサーバーにログインが置かれていない

GET /v1/rooms/:roomKey/ws ​

WebSocketにつなぎ替えます(101)。つなぎ替える前に断るときは、JSONで理由を返します。

状態コード起きるとき
400invalid-id部屋の鍵の形が違う
403origin-not-allowedOriginが許可リストにない
426upgrade-requiredWebSocketの要求でない
429rate-limited送信元から部屋を作る・入る・削除する回数が上限を超えた

部屋が無いとき(部屋を消しているあいだも)と、部屋が受けるWebSocketの数(64本)に達しているときは、つなぎ替えてからerror(room-not-found・room-full)を送って閉じます。HTTPのまま断ると、ブラウザには理由が読めないためです。

トークンやパスワードはURLに載せません。パスワードは、つないだあとのhelloで送ります。

部屋のアセット ​

部屋のアセットのトークン ​

部屋のアセットのAPIには、どれもAuthorization: Bearer <アセットのトークン>が要ります。ログインのアクセストークンでは通りません。

トークンは、部屋のサーバーが席に渡します。部屋に入ったときのwelcomeに、assetTokenとassetTokenExpiresAt(Unixミリ秒)が付いてきます。トークンには、その部屋の鍵、席の役割、ユーザーID(uid)が入っています。署名はHS256で、鍵はSESSION_SECRETです。15分で切れます。

Axeは切れる前に、WebSocketで{"type":"asset-token"}を送り、渡し直してもらいます。サーバーは{"type":"asset-token","assetToken":"…","expiresAt":…,"role":"pl"}で答えます。roleは、その席のいまの役割です。PLをGMにしたときのように役割が変わったときも、サーバーから新しい役割のトークンを送ります。

読む(HEAD・GET)のは、その部屋の席にいる人ならだれでも(GM・PL・見学)できます。置く(PUT)のはGMとPLだけで、見学のトークンには403 not-a-playerを返します。消す(DELETE)のはGMだけです(→ DELETE)。PUTのときは、部屋がまだあり、その人がいまGMかPLとして席にいるかを部屋のサーバーに尋ねます。トークンの期限が残っていても、部屋を削除したあとは404 room-not-found、見学に変えられた人や部屋から抜けた人には403 not-a-playerを返します。Vercel版では、部屋を受け持つインスタンスが替わってから、その人が部屋に入り直すまでのあいだも、GMやPLに403 not-a-playerを返します。入り直したあとに試し直せば置けます。トークンが無いとき、切れたとき、ほかの部屋のものだったときは、401 asset-token-requiredを返します。ハッシュや部屋の鍵を知っていても、トークンが無ければ読めません。部屋を抜けた人が読めるのは、持っているトークンが切れるまで(長くて15分)です。

HEAD /v1/rooms/:roomKey/assets/:sha256 ​

置かれているかを返します。中身は返さず、置き場所からファイルを開くこともしません。Cloudflare版では、一度GETで配ってキャッシュにあるものは、GETと同じくキャッシュから答えます。このときはR2を読まず、読み出しの上限にも数えません。

状態意味
200置かれている。ヘッダーはGETと同じ
401アセットのトークンが無いか、使えない
404その部屋に置かれていない
429送信元からの読み出しが上限を超えた
sh
curl -I https://backend.example.com/v1/rooms/<部屋の鍵>/assets/<sha256> \
  -H "Origin: https://axe.example.com" \
  -H "Authorization: Bearer <アセットのトークン>"

GET /v1/rooms/:roomKey/assets/:sha256 ​

中身を返します。返すのは、その部屋のトークンを持っている人(いま部屋にいる人)にだけです。

返すヘッダー ​

ヘッダー値
Content-Type置いたときに中身から判定した形式(例:image/png)
Content-Length大きさ(バイト)
ETag置き場所(R2かVercel Blob)がつけた値
Cache-Controlprivate, max-age=604800, immutable。ブラウザには7日置かせ、共有のキャッシュには置かせない
Accept-Rangesbytes
X-Content-Type-Optionsnosniff
Access-Control-Allow-OriginリクエストのOrigin
X-Asset-Name置いたときの名前をencodeURIComponentしたもの。名前が無ければ付けない
Access-Control-Expose-HeadersX-Asset-Name。ページから名前を読めるように

SVGのときだけ、Content-Security-Policy: default-src 'none'; img-src data:; style-src 'unsafe-inline'; sandboxも付けます。理由は安全のしくみにあります。

条件付きリクエストと範囲指定 ​

リクエストのヘッダー動き
If-None-Match(ETagと一致)、If-Modified-Since304(本文なし)
If-Match、If-Unmodified-Since(合わない)412 precondition-failed
Range: bytes=10-19206とContent-Rangeで、その範囲だけ返す

If-None-Matchは、弱いETag(W/"…")、複数のETagの並び、*も受け付けます。置かれていないものには、条件にかかわらず404を返します。ただし、運営する人が置かせないようにしたもの(→ 管理画面とBAN)には、451 blockedを返します。HEADも同じです。

キャッシュ(Cloudflare版) ​

Cloudflare版では、同じデータセンターからの2回目以降のGETは、CloudflareのCache APIから返し、R2を読みません(*.workers.devでは効きません)。同じURLへのHEADにも、条件付きリクエストと範囲指定にも、キャッシュから答えます。キャッシュから返すときも、先にトークンを確かめます。Vercel版には、このキャッシュはありません。

PUT /v1/rooms/:roomKey/assets/:sha256 ​

中身をその部屋に置きます。

http
PUT /v1/rooms/<部屋の鍵>/assets/<sha256>
Origin: https://axe.example.com
Authorization: Bearer <アセットのトークン>
Content-Length: 12345
X-Asset-Name: dragon.png

(画像か音声のバイト列)
  • Originは、ACCESS_CONTROL_ALLOW_ORIGINに書いたOriginのどれかでなければなりません。
  • その部屋のGMかPLのアセットのトークンが要ります。見学のトークンには403 not-a-playerを返します。
  • 部屋がまだあり、その人がいまGMかPLとして席にいるかを部屋のサーバーに尋ねます。部屋が無ければ404 room-not-found、見学に変えられているか抜けていれば403 not-a-playerを返します(Vercel版で受け持ちが替わった直後も同じ。入り直したあとに試し直せば通ります)。その部屋にすでに置かれているものは、何も書かないので、尋ねずに200(exists)を返します。
  • X-Asset-Nameには、ファイル名をencodeURIComponentしたものを付けられます。255文字を超える分は切り(2つで1文字のものは半分で切らない)、読めなければ空にします。置き場所のメタデータ(name)に置き、一覧のnameとして返します。HEADとGETの答えにも、同じヘッダーで付きます。
  • Content-Lengthが要ります。本文は読みためずに、そのまま置き場所へ流します。
  • 宣言されたContent-Typeは見ません。中身の先頭のバイトで形式を判定します。
  • 本文のSHA-256が名前と合わなければ、何も置かずに400 hash-mismatchを返します。
  • 中身を置いているあいだに部屋が削除されたときは、置いたものを消して404 room-not-foundを返します。
状態本文意味
201{"id":"<sha256>","status":"stored"}置いた
200{"id":"<sha256>","status":"exists"}すでに置かれていた(書き直さない)

受け付ける形式と上限 ​

種類形式上限
画像PNG・JPEG・GIF・BMP・WebP・AVIF・HEIC・SVG2MiB
音声MP3・WAV・Ogg・FLAC・M4A・AAC10MiB

SVGは、BOM・XML宣言・コメント・DOCTYPEを読み飛ばしたあと、ルート要素が<svgで始まるものです。これらの前置きは、先頭から4KiBに収まっている必要があります。MP4やMOVの動画は受け付けません。理由は安全のしくみにあります。

DELETE /v1/rooms/:roomKey/assets/:sha256 ​

部屋に置いてある画像か音声を1つ、部屋から消します。消せるのは、その部屋のGMと、それを足したPLです。

http
DELETE /v1/rooms/<部屋の鍵>/assets/<sha256>
Origin: https://axe.example.com
Authorization: Bearer <アセットのトークン>

204を返します。その部屋に置かれていなかったときも204です。

  • トークンに入っている役割は見ません。その人がいまGMとして席にいるか、PLとして席にいて部屋のasset-ownerの記録に足した人として名指しされているかを、部屋のサーバーに尋ねます(→ 部屋のプロトコル)。どちらでもない人、見学、部屋から抜けた人には403 not-game-masterを返します。Vercel版では、部屋を受け持つインスタンスが替わってから、その人が部屋に入り直すまでのあいだも同じです。部屋をほかのインスタンスが受け持っているときは、Redisに書かれた記録を読むので、書いた直後の記録は届いていないことがあります。
  • 部屋が無ければ404 room-not-foundを返します。
  • 送信元ごとのアップロードの回数に数えます。超えると429 rate-limitedを返します。
  • 運営する人が画像と音声を置くことを止めているあいだも、消せます。
  • 消したあとは、その部屋の席がHEADやGETで読んでも404 not-foundです。ただしCloudflare版では、データセンターのキャッシュに残った写しを返すことがあります(→ キャッシュ)。
  • 同じものを、また置くことは止めません。二度と置かせないようにできるのは、運営する人だけです(→ アップロード)。

GET /v1/rooms/:roomKey/assets ​

その部屋に置いてあるアセットの一覧を返します。だれのカタログにも載っていないのに部屋には残っている画像や音声を、部屋に入った人が取りに行くためのものです。部屋のアセットのほかのAPIと同じく、その部屋のアセットのトークンが要ります。

json
{
  "assets": [
    {
      "id": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
      "type": "image/png",
      "size": 1024,
      "name": "dragon.png"
    }
  ]
}
フィールド内容
id中身のSHA-256(小文字の16進)
type置いたときに中身から判定した形式(例:image/png)
size大きさ(バイト)
name置いたときにX-Asset-Nameで付いてきた名前。無ければ空文字
  • 応答にはCache-Control: private, no-storeを付けます。
  • 送信元ごとの読み出しの上限(READ_LIMITER)に数えます。超えると429 rate-limitedを返します。
  • 返すのは1万件までです。

置き場所へ直接上げる ​

1回の要求の本文に上限のある部屋のサーバーは、welcomeのassetUploadでそれを知らせます。Vercel版は、Vercelの本文の上限(4.5MB)より少し小さい4,000,000バイトをmaxPutBytesにしています。assetUploadが無ければ(Cloudflare版)、種類ごとの上限(画像2MiB、音声10MiB)まで1回のPUTで置けます。

json
{ "maxPutBytes": 4000000, "directUpload": true }

maxPutBytesを超えるものは、PUTでは413 too-largeになります。directUploadがtrueなら、置き場所(Vercel Blob)へ直接上げます。

  1. POST /v1/rooms/:roomKey/assets/:sha256/uploadsに、{"size":<大きさ>,"name":"<ファイル名>"}を送ります。アセットのトークンを付けます。
  2. 201で{ticket, upload:{url, method, headers, expiresAt}}が返ります。すでに置かれていれば、PUTと同じ200("status":"exists")です。
  3. upload.urlへ、upload.method(PUT)とupload.headersで、中身だけを送ります。アセットのトークンは付けません。
  4. POST /v1/rooms/:roomKey/assets/:sha256/uploads/completeに、{"ticket":"<ticket>"}を送ります。アセットのトークンを付けます。
  5. 部屋のサーバーが中身の形式・大きさ・SHA-256を確かめ、置けたら201("status":"stored")を返します。
  • 確かめる順とコードはPUTと同じです。数を絞るのは、1で尋ねたときだけです。
  • 部屋の画像と音声の合計(ROOM_STORAGE_LIMIT_MB)は、1で尋ねたときと、4で知らせたときの両方で確かめます。続けて受け取ったticketは、どれも互いを数えずに通るためです。
  • ticketは、部屋・アセット・その人・大きさの約束で、15分で切れます。ほかの部屋やアセット、ほかの人のものは400 invalid-requestになります。
  • uploadの先が使えるのはexpiresAt(Unixミリ秒)までです。置き場所が、大きさと期限と上書きしないことを確かめます。
  • 上げていないうちに知らせると404 not-foundです。上げたものは、確かめたあと(置けても置けなくても)消します。
  • 1つのticketで知らせられるのは1回だけです。上げていないうちに知らせた(404 not-found)ときだけは、上げてからもう一度知らせられます。置けたあとに知らせ直すと200("status":"exists")、置けなかったあとは400 invalid-requestです。置けなかったときは、1からやり直します。
  • 直接上げる流れの無い部屋のサーバーでは、どちらのパスも404 not-foundです。

通報 ​

POST /v1/rooms/:roomKey/reports ​

部屋にいる人が、部屋の画像か音声、または部屋そのものを、運営する人に知らせます。部屋のアセットのトークン(→ 部屋のアセットのトークン)で呼び、GM・PL・見学のだれでも通報できます。

json
{ "target": "asset", "assetId": "0f0f…(SHA-256)", "reason": "他人の描いた絵を、作者に断らずに置いています" }
フィールド型内容
target文字列asset(画像か音声)かroom(部屋)
assetId文字列assetのときだけ。その画像か音声のSHA-256
reason文字列理由。前後の空白を除いて1〜500文字

201で{"id":"…"}を返します。部屋のサーバーは、通報した人(送信元のIPアドレス、ログインしていればDiscordのユーザーID、部屋のユーザーIDと役割)も一緒に残します。虚偽の通報をくり返す人を見分けて、BANできるようにするためです(→ 管理画面とBAN)。

  • 形が違えば400 invalid-request、トークンが無いか違う部屋のものなら401 asset-token-requiredを返します。
  • 送信元ごとに、部屋を作る・入ると同じ回数の上限(1分に30回)に数えます。超えると429 rate-limitedです。
  • 同じ人(ログインしていればDiscordのアカウント、していなければ送信元。IPv6は/64ごと)の通報は、24時間に20件まで預かります。超えると429 rate-limitedを返し、預かりません。
  • 預かる通報は1000件までです。超えたときは、ログインしている人の通報としていない人の通報のうち、半分を超えているほうから消します。その中で、いちばん多く預けている人(同じ数なら、いちばん古い通報のある人)の、いちばん古い通報から消します。ログインしていない人は、送信元をIPv6なら/48、IPv4なら/24でまとめて1人と数えます。
  • こうすると、片方から通報を送り続けても、もう一方の通報は半分まで残ります。同じ側の中では、多く送った人の通報から消えます。
  • BANされたアカウントのトークンには403 bannedを返します。

ログインとアカウント ​

ログインの流れと、トークンの扱いは、開発者向けの部屋のサーバーの中の動きにあります。ここでは、Axeが呼ぶAPIの形を書きます。

POST /v1/auth/token ​

json
{ "grant_type": "login_code", "code": "…", "code_verifier": "…" }
json
{ "grant_type": "refresh_token", "refresh_token": "…" }

どちらも、うまくいけば次を返します。

json
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.…",
  "expiresIn": 900,
  "refreshToken": "…",
  "account": {
    "userId": "acct_8c5b7f0e-2c1d-4e3f-9a8b-7c6d5e4f3a2b",
    "name": "Kai",
    "avatarUrl": "https://cdn.discordapp.com/avatars/…"
  }
}

account.userIdは、アカウントを消すまでは、同じDiscordのアカウントならいつも同じです。消したあとに同じDiscordでログインすると、新しいアカウントとして別のuserIdになります。acct_で始まり、ログインしていない人はこの形のuserIdを名乗れません。部屋のhelloのuserIdにはこれを使います。手札やインベントリが、端末やタブをまたいでも同じ人のものになります。

状態コード起きるとき
400invalid-request本文の形が違う
400invalid-grantコードかリフレッシュトークンが、無効・使用済み・期限切れ。運営する人がそのDiscordのアカウントをBANした
429rate-limited送信元から1分に60回を超えた
503login-unavailableログインが置かれていない

アカウントを消す ​

DELETE /v1/accountで、その人の情報(Discordの名前とアバターのURL)と、すべてのトークンを消します。すでに出したアクセストークンは、切れるまでの最大15分は使えます。部屋につながっている人も、トークンが切れて1分を過ぎると部屋から外れます(新しいトークンを受け取れないため)。部屋の席の表には、消したアカウントのuserIdだけが残り、部屋が消えるときに一緒に消えます。Axeはログインした人のカーソルにアカウントの名前とアバターを使うので、部屋に残る発言には、書いたときの名前とアバターの画像のURLが載ったままです。これも部屋が消えるまで残ります。

GETとDELETE /v1/accountは、アクセストークンが無いか無効なら401 login-requiredを返し、アカウントは消しません。トークンのユーザーIDがいまのアカウントと同じときだけ答え、違えば(別の端末で消したあとも)404 not-foundを返します。Axeは、401は消せなかったものとしてログインを残し、404は消せたものとします。消したあと同じDiscordのアカウントでログインし直すと、新しいアカウントが作られます。前のアカウントの残ったアクセストークンでは、新しいアカウントを見ることも消すこともできません。

ログインに失敗したとき ​

戻り先のフラグメントに、login_errorが付きます。

login_error起きるとき
deniedDiscordの同意の画面で断った
provider-failedDiscordとのやりとりに失敗した
rate-limited送信元からのログインが1分に60回を超えた
banned運営する人が、送信元かDiscordのアカウントをBANした
paused運営する人が、新しくログインすることを止めている(緊急停止)

ログインを始めてから10分のうちにDiscordから戻らなかったとき(Discordのアカウントを作っていた、など)は、戻り先がわからないので、Axeには戻りません。部屋のサーバーの400(invalid-request)の画面で止まるので、Axeからログインをやり直してください。SESSION_SECRETをそのあいだに替えたときも同じです。

管理画面のAPI ​

管理画面(/admin)が呼ぶAPIです。ADMIN_DISCORD_IDSを入れたときだけあり、画面の使いかたは管理画面とBANにあります。管理画面のページから使うためのもので、スクリプトから呼ぶ鍵はありません。

  • 入るには、GET /admin/loginからDiscordでログインし(戻り先は/admin/login/callback)、そのあとパスキーで確かめます(下の表)。ログインは、HttpOnlyの__Host-axe-admin Cookie(HTTPSでないときはaxe-admin)に置きます。
  • どれも、X-Admin-Request: 1のヘッダーが要ります。無いか、Originが管理画面のものでないときは403 forbiddenを返します。CORSのヘッダーは付けないので、ほかのサイトのページからは呼べません。
  • ログインが無いか、切れたか、ADMIN_DISCORD_IDSから外されたか、すべての人をログアウトさせたあとのものは、401 login-requiredを返します。
  • パスキーで確かめる前のログインは、sessionとパスキーの4つのほかは403 passkey-requiredです。
  • 部屋の削除、画像と音声の削除、二度と置かせないことと、その解除、アカウントの削除、パスキーの追加と削除、すべての人をログアウトさせることは、パスキーで確かめてから5分を過ぎていると403 reauth-requiredを返します。パスキーで確かめ直してから、もう一度呼びます。
  • BANの対象になりません。
  • ログインの3つが入っていないときは、503 login-unavailableを返します。
  • どの答えにもCache-Control: no-storeを付けます。
メソッドパス内容
GET/admin/api/rooms部屋の一覧。新しいものから2000件まで。作った人のDiscordのIDとIPアドレス付き
GET/admin/api/rooms/:roomKey/seatsその部屋にいま入っている人
GET/admin/api/rooms/:roomKey/assetsその部屋に置いてある画像と音声(ID、種類、大きさ、名前)
DELETE/admin/api/rooms/:roomKey部屋を削除する(GMでなくても)。204、無ければ404 room-not-found
GET/admin/api/bansBANの一覧
POST/admin/api/bansBANする。201で、足したBANを返す
DELETE/admin/api/bans?kind=…&value=…BANを解く。204、無ければ404 not-found
GET/admin/api/activity記録。新しいものから
GET/admin/api/assets?cursor=…置き場所にあるすべての部屋のアセット(部屋の鍵、ID、種類、大きさ、名前、置いた時刻)を1ページずつ。続きがあればcursorを返す
GET/admin/api/rooms/:roomKey/assets/:sha256/contentそのアセットの中身。Content-Security-Policy: default-src 'none'; sandboxを付ける。見たことを記録に残す
DELETE/admin/api/rooms/:roomKey/assets/:sha256その部屋からアセットを消す。204
POST/admin/api/assets/:sha256/block二度と置かせないようにし、本文のrooms(部屋の鍵の配列、2000件まで)から消す。消せた数と消せなかった数を返す
GET/admin/api/blocked二度と置かせないもののIDの一覧
DELETE/admin/api/blocked/:sha256また置けるようにする。204
GET/admin/api/reports参加者からの通報。新しいものから。通報した人も含む
PUT/admin/api/reports/:id通報の状態を変える。本文は{"status":"done"}の形(open・done・dismissed)
GET/admin/api/pause緊急停止で止めているもの
PUT/admin/api/pause緊急停止で止めるものを決める。本文は{"rooms":true,"uploads":false,"logins":false}の形
GET/admin/api/tighten回数の上限をどれだけ締めているか
PUT/admin/api/tighten回数の上限を締める。本文は{"upload":4,"read":1,"room":2,"auth":1}の形(それぞれ1・2・4・10)
GET/admin/api/accounts/:discordIdそのDiscordのユーザーIDのアカウント。無ければ404 not-found
DELETE/admin/api/accounts/:discordIdそのアカウントを削除する。204、無ければ404 not-found
GET/admin/api/settingsいまの設定。秘密の値は、入っているかどうかだけ
GET/admin/api/storage置き場所の使用量(最後に数えたときの大きさ・件数・時刻)と、知らせる量(ADMIN_STORAGE_ALERT_GB)

ログインとパスキーのためのものです。

メソッドパス内容
GET/admin/loginDiscordへ送る(ページとして開く)
GET/admin/login/callbackDiscordから戻る。入れないときは、/admin#denied=<理由>へ戻す
POST/admin/api/logoutこのログインを終える。204
GET/admin/api/sessionだれが、どこまで確かめたか(stageはdiscordかpasskey)
POST/admin/api/passkeys/optionsパスキーを作るための値(navigator.credentials.createに渡す)
POST/admin/api/passkeys作ったパスキーを登録する。ログインも確かめたことになる。201
POST/admin/api/passkeys/challengeパスキーで確かめるための値(navigator.credentials.getに渡す)
POST/admin/api/passkeys/verifyパスキーの署名で確かめる。204
GET/admin/api/passkeys自分のパスキーの一覧
DELETE/admin/api/passkeys/:id自分のパスキーを消す。最後の1つは409 last-passkey
POST/admin/api/sessions/revokeすべての運営する人のログインを終える。204

POST /admin/api/bansの本文は、次の形です。

json
{ "kind": "ip", "value": "203.0.113.0/24", "reason": "部屋を作り続けた", "hours": 24 }
フィールド型内容
kind文字列ipかdiscord
value文字列IPアドレスか範囲(→ BANする)、またはDiscordのユーザーID
reason文字列省けば空。200文字まで
hours整数BANが続く時間(1以上)。省けば、解くまで続く

valueは、置くときの形に直して返します(203.0.113.9/24は203.0.113.0/24、IPv6のアドレスは先頭の/64)。解くときのvalueには、返ってきた形を使います。形が違うときは400 invalid-requestを返します。

エラー ​

本文は{"error":"<コード>"}の形です。

状態コード起きるとき
400invalid-id名前が64文字の小文字16進でない、部屋の鍵の形が違う
400invalid-request本文がJSONでない、名前やパスワードの形が違う。ログインの戻り先が許されていない
400invalid-grantログインのコードやリフレッシュトークンが、無効・使用済み・期限切れ
400hash-mismatch本文のハッシュが名前と合わない
400invalid-challenge管理画面のパスキーの確かめで、値が古いか、使用済みか、ほかのログインのもの
400passkey-refused管理画面で、作ったパスキーを受け付けられなかった
401login-required部屋を作る・削除する、アカウントを見る・消すのに、アクセストークンが無いか無効。管理画面のAPIに、ログインが無いか切れている
401asset-token-required部屋のアセットのトークンが無い・無効・期限切れ・ほかの部屋のもの
401passkey-refused管理画面で、パスキーの署名で確かめられなかった
403origin-not-allowedOriginがACCESS_CONTROL_ALLOW_ORIGINにない
403banned運営する人が、送信元かDiscordのアカウントをBANした(→ 管理画面とBAN)
403forbidden管理画面のAPIを、X-Admin-Requestのヘッダー無しか、ほかのOriginから呼んだ
403passkey-required管理画面で、パスキーで確かめる前のログインで呼んだ。すでにパスキーがあるのに、確かめずに登録しようとした
403reauth-required管理画面の戻せない操作の前に、パスキーで確かめてから5分を過ぎている
403not-game-master部屋を削除しようとしたが、その部屋のGMでない。部屋のアセットを消そうとしたが、いまGMとしても、それを足したPLとしても席にいない
403not-a-player見学のトークンで部屋のアセットを置こうとした。置くときに見学に変えられていたか、部屋から抜けていた。Vercel版で受け持ちが替わってから入り直すまでのあいだ
404not-foundその部屋にアセットが置かれていない(部屋と一緒に消えたときも)。アカウントがもう無い
404room-not-found部屋が無い(作られていないか、消えた)
409passkey-exists管理画面で、もう登録してあるパスキーを登録しようとした
409no-passkeys管理画面で、パスキーが無いのに確かめようとした
409last-passkey管理画面で、最後のパスキーを消そうとした
409too-many-passkeys管理画面で、11個目のパスキーを登録しようとした
411length-requiredPUTにContent-Lengthがない
412precondition-failedGETのIf-MatchかIf-Unmodified-Sinceに合わない
413too-large種類ごとの上限か、maxPutBytesを超えている
413room-storage-full置くと、部屋の画像と音声の合計がROOM_STORAGE_LIMIT_MBを超える(→ 設定項目)
415unsupported-type受け付ける画像でも音声でもない
426upgrade-required部屋に入るURLを、WebSocketでなく開いた
429rate-limited送信元からの、アップロード、読み出し、部屋を作る・削除する・入る、ログインのどれかが上限を超えた
451blocked運営する人が、その名前のものを置かせないようにした
500config-invalid設定が足りないか、形が違う(→ 設定項目)
500internal-errorそれ以外の失敗(本文がContent-Lengthと合わないなど)
503login-unavailableログインの3つが入っていない
503paused運営する人が、部屋を作ることか部屋のアセットを置くことを止めている(→ 緊急停止)

config-invalidのときは、足りない設定の名前がログに出ます。ADMIN_DISCORD_IDSを入れていないときは、管理画面と管理画面のAPIに404 not-foundを返します。WebSocketでつないだあとに接続を閉じる理由は、サーバー版で困ったときにあります。

MITライセンスで公開しています