Skip to content

Avatars

VirMesh のアバター manifest を解決し、署名と短命 grant で安全に取得する方法を説明します。

アバターの重い asset は AvatarServer から分離して配信できます。クライアントは me.virmesh.avatar.resolveAvatar で署名付き manifest を解決し、manifest 内の URL からモデル、サムネイル、WASM script を取得します。

Model

アバターは avatarId を持ちます。

text
medi:avatar:<scheme>:<publicKey>

avatarId の公開鍵は manifest 署名の検証に使います。publisher の player 鍵は avatarId と publisher id を含む delegation payload に署名します。

この分離により、publisher は player identity を保ったまま、アバターごとに別の鍵を使えます。handle は表示用 snapshot として manifest に入れられますが、権威情報は player id です。

json
{
  "avatarId": "medi:avatar:ed25519:avatar-public-key",
  "publisherDelegation": {
    "payload": {
      "avatarId": "medi:avatar:ed25519:avatar-public-key",
      "publisher": {
        "id": "medi:player:ed25519:publisher-public-key",
        "handle": "[email protected]"
      },
      "issuedAt": 1770000000
    },
    "signature": "base64-signature-by-publisher"
  }
}

publisherDelegation.signature は canonical JSON of publisherDelegation.payload に対する publisher の署名です。manifest response の signature は canonical JSON of payload.manifest に対する avatar 鍵の署名です。

Public avatars

public avatar は、同じ manifest と asset URL を複数の wearer が参照できます。これは VRChat の public avatar に近い使い方です。

クライアントは次の順で処理します。

  1. wearer の presence から avatarReference を読む。
  2. avatarReference.endpointme.virmesh.avatar.resolveAvatar を呼ぶ。
  3. response の manifest 署名を avatarId から検証する。
  4. publisherDelegation を publisher id から検証する。
  5. manifest 内の asset を取得し、hash が一致するときだけロードする。

manifest には形式を固定しません。asset entry は contentTypeprofileurlhashsize を持ちます。

json
{
  "assets": [
    {
      "kind": "model",
      "contentType": "model/gltf-binary",
      "profile": "glb",
      "url": "https://cdn.example.com/avatars/avatar.glb",
      "hash": "sha256:base64url-hash",
      "size": 2480000
    }
  ]
}

versionId または hash を指定して解決した場合、クライアントは manifest と asset の hash を必ず確認します。一致しない asset はロードしません。

Private avatars

private avatar は、world relay 経由で wearer が短命 fetchGrant を発行した場合だけ取得できます。これは VRChat の private avatar に近い制御ですが、認可は wearer の署名で表現します。

最小フローは次です。

  1. viewer は同じ world/instance に参加している wearer の avatar を表示したい。
  2. viewer は WorldServer または InstanceServer の relay に fetchGrant 要求を送る。
  3. relay は viewer が同じ world/instance の参加者であることを確認し、wearer client に中継する。
  4. wearer client は avatarFetchGrant.payload を署名して viewer に返す。
  5. viewer は fetchGrantme.virmesh.avatar.resolveAvatar に添えて manifest を解決する。
  6. AvatarServer は grant の署名、発行時刻、viewer、wearer、avatar 条件を検証する。

fetchGrantissuedAt から 60 秒程度の短い許容時間だけ受け付けます。relay は world/instance の参加確認を行いますが、その context は fetchGrant.payload には入れません。

json
{
  "payload": {
    "avatarId": "medi:avatar:ed25519:avatar-public-key",
    "versionId": "2026-04-11T00:00:00Z",
    "wearerId": "medi:player:ed25519:wearer-public-key",
    "viewerId": "medi:player:ed25519:viewer-public-key",
    "issuedAt": 1770000000
  },
  "signature": "base64-signature-by-wearer"
}

private asset URL は grant-bound URL または短命 signed URL にします。viewer は fetchGrant を CDN request に添えず、manifest 内の URL をそのまま取得します。manifest だけを保護して asset が恒久公開 URL から取れる形にはしません。

json
{
  "assets": [
    {
      "kind": "model",
      "contentType": "model/gltf-binary",
      "profile": "glb",
      "url": "https://cdn.example.com/avatars/private/avatar.glb?token=short-lived-asset-token",
      "hash": "sha256:base64url-hash",
      "size": 2480000
    }
  ]
}

Purchase receipts

購入証明は avatarPurchaseReceipt として扱います。これは seller が buyer に対して発行した検証可能な receipt claim です。

v1 の receipt は「正規販売者から購入された」ことまでは保証しません。クライアントや viewer は receipt の seller、buyer、shop URL、対象 avatar を検証し、seller を信頼するかを自分で判断します。

json
{
  "payload": {
    "avatarId": "medi:avatar:ed25519:avatar-public-key",
    "shopUrl": "https://shop.example.com/items/alice-avatar",
    "seller": {
      "id": "medi:player:ed25519:seller-public-key",
      "handle": "[email protected]"
    },
    "buyer": {
      "id": "medi:player:ed25519:buyer-public-key",
      "handle": "[email protected]"
    },
    "issuedAt": 1770000000
  },
  "signature": "base64-signature-by-seller"
}

他の player は、提示された receipt が buyer id と一致しているかを確認できます。提示がない場合に分かるのは、検証可能な正規 receipt を提示していないことです。

Avatar scripts

WASM script は manifest の script に入れます。v1 では host API を定義せず、sandbox と permission model だけを固定します。

json
{
  "script": {
    "url": "https://cdn.example.com/avatars/avatar-script.wasm",
    "hash": "sha256:base64url-hash",
    "permissions": ["avatar.self"],
    "sandbox": {
      "runtime": "wasm",
      "network": false,
      "filesystem": false,
      "worldMutation": false,
      "maxMemoryBytes": 16777216,
      "maxExecutionMillis": 5
    }
  }
}

クライアントは script 実行を拒否できます。v1 では network access、filesystem access、world mutation は許可しません。world への影響や他 player との高権限 interaction は、別の host API 仕様で扱います。

Next steps