Skip to content

Transport

VirMesh の HTTP action を正しい envelope と署名ルールで送る方法を説明します。

ここで扱うルールは PlayerServer の共通 transport と、共有実装の handle route に対応しています。

HTTP action families

PlayerServer には 3 つの action family があります。

FamilyTransportCanonical path
publicHTTP GET or POST/public/:action
privateHTTP POST/private/:action
ws-syncWebSocket upgrade/ws-sync/:action

catalog は GET /public/, GET /private/, GET /ws-sync/ で取得できます。

Public actions

public action は action ごとに GET query か POST JSON のどちらかを使います。

  • GET action は query string を使います
  • POST action は JSON body を使います

たとえば me.virmesh.player.resolveProfileGET で、id または handle のどちらか一方だけを query に付けます。

text
GET /public/me.virmesh.player.resolveProfile?id=medi:player:ed25519:base64-public-key
GET /public/me.virmesh.player.resolveProfile?handle=alice@example.com

一方、me.virmesh.register.startChallenge のような public POST action は top-level に payload を持つ JSON object を送ります。

json
{
  "payload": {
    "type": "passkey"
  }
}

me.virmesh.account.disableAccount のように top-level signature を追加で要求する public action もあります。

json
{
  "payload": {
    "accountId": "medi:player:ed25519:base64-public-key",
    "disabled_at": 1770000000
  },
  "signature": "base64-signature-of-payload"
}

この signature は canonical JSON of payload に対する本人鍵署名です。

Signed resolveProfile Response

me.virmesh.player.resolveProfile200 response は shared status envelope ではありません。 top-level の response 署名は持たず、対象 player 本人が handle と各 profile module を個別署名した shape を返します。

json
{
  "payload": {
    "handle": {
      "record": {
        "id": "medi:player:ed25519:base64-public-key",
        "primaryHandle": "alice@example.com",
        "secondaryHandles": ["alice@players.example"],
        "playerServer": "https://ps.example.com/",
        "updated_at": 1770000100
      },
      "signature": "base64-signature-by-player-for-handle-record"
    },
    "modules": {
      "profile+me.virmesh.player.displayName": {
        "payload": {
          "module": "profile+me.virmesh.player.displayName",
          "id": "medi:player:ed25519:base64-public-key",
          "displayName": "Alice",
          "updated_at": 1770000200
        },
        "signature": "base64-signature-by-player-for-display-name-module"
      },
      "profile+me.virmesh.player.card": {
        "payload": {
          "module": "profile+me.virmesh.player.card",
          "id": "medi:player:ed25519:base64-public-key",
          "bio": "VR world builder",
          "image": {
            "assetId": "profimg_123",
            "contentType": "image/png",
            "hash": "sha256:base64url-hash",
            "width": 512,
            "height": 512,
            "size": 42000
          },
          "updated_at": 1770000300
        },
        "signature": "base64-signature-by-player-for-profile-card-module"
      }
    }
  }
}

クライアントは次を検証します。

  1. payload.handle.signature が canonical JSON of payload.handle.record に対する署名であること
  2. payload.modules.*.signature が対応する payload.modules.*.payload に対する署名であること
  3. payload.handle.record.idpayload.modules.*.payload.id が一致すること

Private Request Envelope

private action は通常 from, payload, signature を要求します。

json
{
  "from": "medi:player:ed25519:base64-public-key",
  "payload": {
    "primaryHandle": "alice@example.com",
    "updated_at": 1770000100
  },
  "signature": "base64-signature"
}

通常の private action では、署名対象は payload 単体ではなく canonical JSON 化した次の object 全体です。

json
{
  "action": "me.virmesh.handle.updateHandle",
  "from": "medi:player:ed25519:base64-public-key",
  "payload": {
    "primaryHandle": "alice@example.com",
    "updated_at": 1770000100
  }
}

updateProfile Exception

me.virmesh.player.updateProfile だけは署名意味論が少し違います。 request は private envelope ですが、signature は envelope ではなく patch 適用後に保存される module payload そのものに対する署名です。

json
{
  "from": "medi:player:ed25519:base64-public-key",
  "payload": {
    "module": "profile+me.virmesh.player.card",
    "set": {
      "bio": "VR world builder"
    },
    "updated_at": 1770000200
  },
  "signature": "base64-signature-for-resulting-module-payload"
}

この request に対して server が保存前に検証する canonical payload の例は次です。

json
{"bio":"VR world builder","id":"medi:player:ed25519:base64-public-key","module":"profile+me.virmesh.player.card","updated_at":1770000200}

Public Fetch Route

保存済み profile image asset は public route から取得します。

text
GET /profile-images/:assetId

詳細な flow は Profile Images を参照してください。

Multipart Asset Upload Actions

me.virmesh.asset.upload などの asset 系 action は Content-Type: multipart/form-data を使います。envelope(JSON metadata + 署名)とバイナリファイルを 1 リクエストで送ります。

envelope part は通常の private action と同じ { from, payload, signature } 構造です。signature は canonical JSON of { action, from, payload } に対する本人署名です。

POST /private/me.virmesh.asset.upload
Content-Type: multipart/form-data; boundary=----Boundary

------Boundary
Content-Disposition: form-data; name="envelope"
Content-Type: application/json

{"from":"...","payload":{"scope":"me.virmesh.player.profileImage","contentType":"image/png","size":42000,"hash":"sha256:base64url-hash","width":512,"height":512},"signature":"..."}

------Boundary
Content-Disposition: form-data; name="file"; filename="profile.png"
Content-Type: image/png

<raw binary bytes>

------Boundary--

server は envelope 署名を検証した後、file part の body が envelope metadata の hashsize に一致することを確認します。

ws-sync

ws-sync action は GET /ws-sync/:action へ WebSocket upgrade して使います。

通常の HTTP GET で叩くと 426 を返します。

json
{
  "status": "status+me.virmesh.http.upgrade_required",
  "payload": {
    "message": "Use a WebSocket upgrade request for /ws-sync/:action."
  }
}

Server-to-Server Federation

他 PlayerServer へのリクエスト転送は、クライアントの envelope をそのまま転送先に POST します。転送元の PlayerServer は X-VirMesh-Forwarded-By ヘッダーを付与します。

POST https://target-ps.example.com/private/me.virmesh.social.sendFriendRequest
X-VirMesh-Forwarded-By: https://sender-ps.example.com
Content-Type: application/json

{ ... client envelope unchanged ... }
  • envelope の frompayloadsignature は一切変更しない
  • X-VirMesh-Forwarded-By は転送元 PS の canonical URL
  • 受信側はこのヘッダーで転送経路を検証できる(v1 ではオプショナル)
  • 詳細は Server Federation を参照

Canonical JSON

VirMesh の署名は stableStringify() による canonical JSON を前提にしています。

規則は次のとおりです。

  1. scalar は JSON.stringify() を使います。
  2. array は要素順を保持します。
  3. object は key を辞書順に並べ替えてから stringify します。
  4. 空白は入れません。

たとえば次の object:

json
{
  "payload": {
    "primaryHandle": "alice@example.com",
    "updated_at": 1770000100
  },
  "from": "medi:player:ed25519:base64-public-key",
  "action": "me.virmesh.handle.updateHandle"
}

署名対象の文字列は次になります。

json
{"action":"me.virmesh.handle.updateHandle","from":"medi:player:ed25519:base64-public-key","payload":{"primaryHandle":"alice@example.com","updated_at":1770000100}}

me.virmesh.player.resolveProfile の response 部分署名でも同じ key-sort ルールを使います。 たとえば payload.handle.record の canonical string は次です。

json
{"id":"medi:player:ed25519:base64-public-key","playerServer":"https://ps.example.com/","primaryHandle":"alice@example.com","secondaryHandles":["alice@players.example"],"updated_at":1770000100}

payload.modules["profile+me.virmesh.player.displayName"].payload の canonical string は次です。

json
{"displayName":"Alice","id":"medi:player:ed25519:base64-public-key","module":"profile+me.virmesh.player.displayName","updated_at":1770000200}

Shared HTTP Error Envelope

共通 error response は次の形です。

json
{
  "status": "status+me.virmesh.json.invalid_json",
  "payload": {
    "message": "Public request must be valid JSON."
  }
}

statusstatus+me.virmesh.* 名前空間を使います。詳細は Errors を参照してください。

Next Steps

  • medi:player:... の書式と鍵の入力形式は Identity を参照してください。
  • profile module 更新、画像 upload、公開プロフィール取得は me.virmesh.player を参照してください。
  • ハンドル route は me.virmesh.handle を参照してください。
  • チャレンジ一覧取得・開始・ポーリング action は me.virmesh.register を参照してください。