# カスタムライセンスシステムの連携ガイド

現在のソースコードは、Licentivo と `custom_http` の2つのライセンスプロバイダーをサポートしています。加盟店は **システム設定 → ライセンスシステム** でいずれかを選択して有効化し、すべての商品と sandbox/live 環境で同じプロバイダーを使用します。カスタム接続とプランのマッピングは、商品と環境ごとに設定します。この機能には独自ライセンスシステムの構築は含まれません。加盟店は引き続き自社のライセンスサービスと SDK を使用します。

既存の Licentivo アカウント交換、9言語の SDK、およびデバイス管理は従来のフローを使用します。カスタム商品は本書のプロトコルで購入状態を同期し、顧客はアカウントセンターでライセンスの提供情報を確認します。Licentivo SDK のフローは使用しません。

## クイックスタート

1. 自社サービスに以下の4つの HTTP インターフェースを実装し、接続ごとに独立した HMAC 署名キーと識別子を設定します。
2. 「ライセンスシステム」で「カスタムライセンスサービス」を選択し、接続を追加して、アドレス、環境、接続識別子、key ID、標準 Base64 キーを入力します。検証後に保存し、「カスタムライセンスサービスを有効化」をクリックします。
3. 商品と販売プランの下書きを作成します。購入タイプと価格は Launch で管理し、ライセンスルールは自社システムで管理します。
4. 商品と環境を選択し、接続を選んで、外部の商品とプランの参照を入力して保存します。プロバイダーはサイト全体の選択で決まり、他の商品で同時に Licentivo を有効化することはできません。
5. 販売プランページでテスト版を公開し、決済、提供、更新、アップグレード、返金、障害復旧の受け入れ確認を完了した後、本番環境を個別に設定します。

アドレス、環境、接続識別子、ネットワークアクセス方針は作成後に変更できません。必要に応じて新しい接続を作成してください。署名キーはローテーションでき、空欄なら既存キーを保持します。別のライセンスシステムの注文が存在する場合、サイト全体のプロバイダーを直接切り替えることはできず、先に移行を完了する必要があります。切り替え後、既存プランはプレビュー専用となり再公開が必要ですが、保存済みの接続資格情報とマッピングは保持されます。注文済み商品の接続や販売中のマッピングは直接変更できません。新しい価格やマッピングには新しいプランバージョンを使用します。

API で設定する場合、まず `PUT /api/v1/admin/licensing/provider` を呼び出し、リクエスト本文に `{"provider":"custom_http"}` または `{"provider":"licentivo"}` を指定してから商品マッピングを保存します。有効化されていないプロバイダーの商品ライセンス紐付けや Licentivo プランポリシーのマッピングは保存できません。

接続チェックではプロトコル、認証、環境、対応購入タイプを確認しますが、加盟店に代わって外部プランを作成または確認しません。本番決済の前に、実際の外部プランで sandbox 連携の受け入れ確認を完了する必要があります。

## 3. 加盟店の連携フロー

1. 既存のライセンスシステムの前に本書の4つのインターフェースを実装し、既存のライセンスロジックは維持します。
2. Launch に「カスタムライセンスサービス」接続を新規作成し、HTTPS アドレス、接続識別子、環境、署名キーを設定します。
3. Launch が能力チェックインターフェースを呼び出し、プロトコルバージョン、永久/期間限定購入の対応、提供方式を確認します。
4. 商品にこの接続を選択し、販売プランバージョンを外部の `product_ref` と `plan_ref` にマッピングします。外部番号には加盟店独自の文字列を使用でき、Licentivo UUID は必須ではありません。
5. sandbox で決済、更新、アップグレード、返金、障害復旧の受け入れ確認を完了し、live の設定を個別に公開します。
6. Launch は決済を検証後、注文、購入のカバー範囲、送信待ちタスクを同一のデータベーストランザクションに保存します。
7. バックグラウンド処理が完全な購入スナップショットをカスタムライセンスサービスに送信します。同サービスは受信記録を永続化してから受信を確認します。
8. ライセンスサービスが目標状態を処理します。Launch は元イベントの受領結果を照会し、`applied` の場合にのみ同期完了を確認します。
9. Launch はログイン済みで購入を所有する顧客の提供情報を読み取り、顧客センターにライセンスコード、アクティベーション入口、またはプレーンテキストの説明を表示します。アプリは加盟店独自のライセンスシステムに接続します。
10. 更新、アップグレード、返金、決済取消、一時停止、期限切れはそれぞれ新バージョンの完全なスナップショットを生成します。ライセンスサービスは最新の目標状態に従って処理します。

同一商品の sandbox/live にはそれぞれ接続を紐付けます。購入が存在する場合、プロバイダーの変更には個別の移行フローが必要です。管理画面の選択に従って古い注文やタスクを新サービスに黙って送信してはいけません。

## 4. カスタムライセンスサービスが実装する API

以下のインターフェースはすべて **加盟店のライセンスサービスが受信し、Launch サーバーが呼び出します**。`base_url` に固定パスプレフィックスを含めることができ、表のパスはその後に追加されます。

| メソッドとパス | 目的 |
| --- | --- |
| `GET /launch/v1/capabilities` | 接続チェック、プロトコルと能力の宣言 |
| `POST /launch/v1/events` | 購入の完全な目標状態を受信し、再試行では元イベントを使用 |
| `GET /launch/v1/events/{event_id}` | イベントが実際に適用されたか照会し、失われた応答を回復 |
| `GET /launch/v1/entitlements/{entitlement_ref}/delivery` | 既存ライセンスの提供情報を読み取り、2つ目のライセンスを作成しない |

初版では受領結果を能動的に照会し、外部コールバックで注文を変更する入口は追加しません。カスタムサービスには決済確認、価格変更、購入期間の書き換え権限はありません。

### 4.1 共通規約と認証

- JSON は UTF-8、時刻は UTC RFC 3339、`version` は正の整数を使用します。
- 成功応答は `{"data": {...}, "request_id": "..."}`、エラー応答は `{"error": {"code": "...", "message": "..."}, "request_id": "..."}` です。
- 環境と接続ごとに独立したキーを使用します。キーは少なくとも 32 バイトのランダム値とし、Launch の既存の暗号化キーストレージから参照します。フロントエンド設定、例、アプリのインストールパッケージには含めません。
- 各リクエストは `X-Launch-Key-Id`、`X-Launch-Timestamp`、`X-Launch-Nonce`、`X-Launch-Signature` を送信します。
- タイムスタンプは10進数の Unix 秒です。nonce は少なくとも 128 ビットのランダム値をパディングなしの base64url でエンコードし、ネットワーク試行ごとに新しい値を生成します。

署名入力は、以下の7項目を1つの改行文字で連結した UTF-8 バイト列で、末尾に改行を追加しません：

```text
v1
<key_id>
<timestamp>
<nonce>
<UPPERCASE HTTP method>
<encoded request path and raw query, including the base_url path prefix>
<lowercase hexadecimal SHA-256 digest of the exact request body>
```

`X-Launch-Signature` は `v1=` に `HMAC-SHA256` の結果をパディングなしの base64url でエンコードしたものを続けます。GET の本文は空です。受信側は逆シリアル化の前に署名と時間窓を検証し、定数時間比較を使用します。デフォルトで前後 300 秒のずれを許容し、リプレイ防止のため nonce を少なくとも 600 秒保持します。再試行間で nonce を再利用しませんが、イベント ID と業務リクエスト本文は変更してはいけません。

署名は HTTPS の代わりではありません。リクエストには顧客のパスワード、決済キー、Licentivo 管理キーを含めません。デフォルトでは匿名アカウント参照のみを送信し、メールアドレスは送信しません。サーバーの HTTP クライアントにはタイムアウト、応答長の上限、資格情報を付けたリダイレクト追跡の禁止が必要です。カスタムアドレス設定時は宛先を検証し、内部ネットワークへのアクセスは管理者が明示的に設定したデプロイ機能とします。

### 4.2 能力チェック

`GET /launch/v1/capabilities` は HTTP 200 を返します。例：

```json
{
  "data": {
    "protocol": "launch-external-licensing-v1",
    "connection_ref": "merchant-01-sandbox",
    "environment": "sandbox",
    "purchase_types": ["perpetual", "fixed_term"],
    "delivery_types": ["license_key", "activation_url", "instructions"],
    "devices": false,
    "identity_exchange": false
  },
  "request_id": "check-001"
}
```

`protocol`、`connection_ref`、`environment`、`purchase_types`、`delivery_types` は必須です。後の2つは空でない配列とし、未対応の購入タイプは販売公開できません。初版では `devices` と `identity_exchange` は `false` でなければなりません。拡張契約と実装が完了して初めて対応を宣言できます。

### 4.3 購入スナップショット

`POST /launch/v1/events` のリクエスト例：

```json
{
  "schema": "launch-external-licensing-v1",
  "event_id": "7bf84b91-2749-4b01-a6a7-bd527c488ee8",
  "connection_ref": "merchant-01-sandbox",
  "environment": "sandbox",
  "type": "entitlement.snapshot",
  "change_reason": "purchase",
  "account_ref": "customer_opaque_001",
  "entitlement_ref": "purchase_opaque_001",
  "launch_product_id": "c6dd463e-50f7-4b76-8239-0a8ec6fc304b",
  "launch_plan_version_id": "5459a073-9362-4b46-949d-9b0c5d84421a",
  "product_ref": "desktop-editor",
  "plan_ref": "professional-yearly-v1",
  "version": 1,
  "desired_state": "active",
  "valid_from": "2026-10-25T00:00:00Z",
  "valid_until": "2027-10-25T00:00:00Z"
}
```

上記のすべてのフィールドは必須です。`valid_until` が `null` の場合は永久購入を意味し、それ以外は `valid_from` より後でなければなりません。

| フィールド | 制約と意味 |
| --- | --- |
| `event_id` | Launch が生成する UUID。同一イベントの再試行では不変 |
| `connection_ref` / `environment` | 署名キーが属する接続と環境に一致する必要があります |
| `account_ref` | 安定した匿名アカウント参照。受信側は接続、環境、この参照の組み合わせで顧客を識別 |
| `entitlement_ref` | 1つの購入関係の安定した参照。すべて取り消された後の再購入には新しい参照を使用 |
| `launch_product_id` / `launch_plan_version_id` | Launch の商品と不変のプランバージョンの UUID |
| `product_ref` / `plan_ref` | 連携先が設定する外部参照。1–128 個の ASCII 文字で、英数字、`_`、`-`、`.` を使用可能。外部プランは自動作成しません |
| `version` | 同じ接続、環境、購入参照のもとで単調増加する目標バージョン |
| `desired_state` | `active`、`suspended`、`expired`、`revoked` |
| `change_reason` | `purchase`、`renewal`、`upgrade`、`refund`、`reversal`、`suspension`、`resumption`、`expiry`、`reconciliation` |

`account_ref` と `entitlement_ref` は 1–128 文字の英数字、アンダースコア、またはハイフンを使用します。`schema` と `type` は例の値に固定します。受信側は `desired_state` と完全な絶対期間に従って結果を適用し、理由に基づいて独自に日数を追加したりライセンス全体を取り消したりしません。

特に返金では、更新やアップグレードの注文を返金した後も有効な基本購入が残る場合があり、スナップショットは `active` のままで、期間が短縮またはプランが変更されることがあります。台帳の再計算後に有効な購入カバー範囲がなくなった場合にのみ、対応する終了状態を送信します。

### 4.4 受信、受領結果、回復

受信側はイベント ID、元のリクエスト本文のダイジェスト、実行待ちタスクを永続化してから HTTP 202 を返す必要があります。同期済みの重複リクエストには HTTP 200 を返せます。どちらも以下の受領結果構造を返します。

```json
{
  "data": {
    "event_id": "7bf84b91-2749-4b01-a6a7-bd527c488ee8",
    "connection_ref": "merchant-01-sandbox",
    "environment": "sandbox",
    "entitlement_ref": "purchase_opaque_001",
    "version": 1,
    "payload_sha256": "<64-character lowercase hexadecimal SHA-256 digest of the exact event body>",
    "status": "pending",
    "error_code": null
  },
  "request_id": "receipt-001"
}
```

ダイジェストのプレースホルダーは実際のダイジェストに置き換えます。受領結果の上記フィールドはすべて必須で、エラーがなければ `error_code` は `null` です。`GET /launch/v1/events/{event_id}` は同じ構造を HTTP 200 で返し、状態は次のいずれかです：

| `status` | 意味 | Launch の処理 |
| --- | --- | --- |
| `pending` | 永続化済み、実際のライセンス変更は未完了 | 照会を延期し、同期完了としない |
| `applied` | ライセンスサービスが目標状態の実際の適用を確認 | 関連する全フィールドとダイジェストを照合後、同期完了を確認 |
| `rejected` | 外部プランが存在しないなど、適用できないことが確定 | 自動再試行を停止し、明確なエラーを表示 |
| `manual_review` | 結果が不確定または状態が競合し、手動確認が必要 | 元のタスクとイベントを保持し、監査可能な対応を待つ |

提供状態は同期受領結果とは独立しており、`applied` を顧客アプリの実行許可と直接みなすことはできません。

信頼性の要件：

- 少なくとも1回の配信。受信側は業務上の冪等性を保証する必要があり、ネットワーク層で「1回しか呼び出さない」と約束することはできません。
- 同一イベント ID と同一の元リクエスト本文には既存結果を返し、同一 ID で本文が異なる場合は HTTP 409 `IDEMPOTENCY_CONFLICT` を返します。
- 古いバージョンは新しいバージョンを上書きできません。同じ購入バージョンに対する異なるイベントや目標を黙って受け入れず、確認用に HTTP 409 `STATE_VERSION_CONFLICT` を返します。
- POST 応答の喪失またはプロセス再起動後、Launch はまず元イベントを GET で照会し、`EVENT_NOT_FOUND` が明示的に返された後にのみ元の ID と本文を再送します。
- リモートでの実際の発行結果が不明な場合、連携先も独自の永続的な冪等参照で確認し、再送だけで新たなライセンスを発行してはいけません。
- HTTP 429 では `Retry-After` に従い、タイムアウトと 5xx はバックオフして回復します。上限超過後は手動確認に移ります。認証、設定、業務上の拒否を同期成功とみなしてはいけません。

推奨エラーコード：`SIGNATURE_INVALID`（401）、`SIGNATURE_EXPIRED`（401）、`NONCE_REPLAYED`（409）、`SCOPE_MISMATCH`（403）、`EVENT_NOT_FOUND`（404）、`IDEMPOTENCY_CONFLICT`（409）、`STATE_VERSION_CONFLICT`（409）、`PLAN_UNKNOWN`（422）、`PURCHASE_TYPE_UNSUPPORTED`（422）、`RATE_LIMITED`（429）、`SERVICE_UNAVAILABLE`（503）。エラーメッセージに資格情報を含めてはいけません。

### 4.5 ライセンスの提供

`GET /launch/v1/entitlements/{entitlement_ref}/delivery` は当該接続と環境の既存の提供結果のみを読み取り、照会を新規発行命令として扱ってはいけません。HTTP 200 の例：

```json
{
  "data": {
    "connection_ref": "merchant-01-sandbox",
    "environment": "sandbox",
    "entitlement_ref": "purchase_opaque_001",
    "version": 1,
    "status": "ready",
    "delivery": {
      "type": "license_key",
      "value": "EXAMPLE-ONLY-NOT-A-REAL-LICENSE",
      "expires_at": null
    }
  },
  "request_id": "delivery-001"
}
```

`status` は `pending`、`ready`、または `unavailable` です。ready 以外では `delivery` は `null` です。ready の場合、`type` は宣言済みの `license_key`、`activation_url`、または `instructions`、`value` は最大 4096 UTF-8 バイト、`expires_at` は UTC 時刻または `null` でなければなりません。応答の関連フィールドはすべて Launch の現在の購入バージョンと一致する必要があり、旧バージョンの提供を現在の結果として扱うことはできません。

ライセンスコードは機密データです。提供応答に `Cache-Control: no-store` を使用し、本文を記録せず、永続化が必要な場合は暗号化ストレージを使用します。アクティベーション URL は接続設定で許可された HTTPS オリジンのみを受け入れます。instructions はプレーンテキストとして表示し、HTML やスクリプトを実行しません。取り消し済み、全額返金済み、または有効な購入がないアカウントは Launch を通じて提供情報を取得し続けることはできません。提供済み資格情報の実際の無効化は外部システムが行います。

## Launch 管理 API と顧客 API

これらのインターフェースは加盟店がデプロイした Launch の origin 配下にあり、外部サービスの HMAC キーによる認証は使用しません。管理者は既存のサイトセッションを使用し、書き込みリクエストには `X-CSRF-Token` が必要です。顧客は自分の購入のみを読み取れます。

| メソッドとパス | 目的 |
| --- | --- |
| `GET /api/v1/admin/licensing/connections` | `connections`、`bindings`、`mappings`、`default_provider` を返します。接続一覧にはカスタム接続のみを含み、Licentivo は従来の管理インターフェースを使用します |
| `POST /api/v1/admin/licensing/connections` | カスタム接続を検証して追加 |
| `PUT /api/v1/admin/licensing/connections/{id}` | 名前、許可されたアクティベーションオリジン、署名設定を編集。過去の受信アドレスは変更しない |
| `POST /api/v1/admin/licensing/connections/{id}/check` | 再チェック。HTTP 200 の `last_check_error` が null 以外ならチェック失敗 |
| `PUT /api/v1/admin/products/{id}/licensing` | 商品の選択環境に対してプロバイダー、接続、外部プランのマッピングを設定する |
| `GET /api/v1/me/products/{id}/delivery?environment=sandbox` | 現在の顧客が現在有効なカスタム購入のライセンス配信を取得する。Cookie または desktop Bearer 認証 |
| `GET /api/v1/licensing/documentation` | このドキュメント。ログイン不要 |
| `GET /api/v1/licensing/protocol` | カスタムサービスが実装する必要のある OpenAPI。ログイン不要 |

カスタム接続を新規追加するリクエスト本文：

```json
{
  "provider": "custom_http",
  "name": "My licensing service",
  "base_url": "https://licenses.example.com",
  "environment": "sandbox",
  "connection_ref": "merchant-01-sandbox",
  "key_id": "key-1",
  "signing_key": "<standard Base64 encoding of 32 to 128 random bytes>",
  "allow_private_network": false,
  "allow_local_http": false,
  "activation_origins": ["https://activate.example.com"]
}
```

レスポンスの `data` は機密情報をマスクした接続オブジェクトで、署名キーや secret ID は返しません。allow_local_http は sandbox の localhost／ループバックアドレスのみ許可します。プライベートネットワークの HTTPS には管理者の明示的な許可が必要です。キーを空欄にできるのは既存の接続を編集する場合のみです。Licentivo は引き続き `POST /api/v1/admin/connections/licentivo` で設定します。

商品を設定するリクエスト本文：

```json
{
  "provider": "custom_http",
  "environment": "sandbox",
  "connection_id": "<id returned when creating the connection>",
  "mappings": [
    {
      "version_id": "<Launch plan version UUID>",
      "product_ref": "desktop-editor",
      "plan_ref": "professional-yearly-v1"
    }
  ]
}
```

Licentivo を選択した場合は `{"provider":"licentivo","environment":"sandbox","mappings":[]}` を送信します。製品／ポリシーのマッピングには引き続き従来の API を使用します。初めて切り替えると既存の公開内容はプレビューになり、新しいプロバイダーに合わせて再確認して公開する必要があります。mappings には追加または維持するバージョンだけを含めることができます。省略しても既存のマッピングは削除されません。

顧客への配信は HTTP 200 の標準 data envelope です。購入がない場合は 404、Licentivo 商品は 409 `LICENSING_DELIVERY_UNSUPPORTED`、未同期、停止、期限切れまたは全額返金の場合は 409 `ENTITLEMENT_NOT_READY`、外部サービスが一時的に利用不可または受領確認が一致しない場合は 503 を返します。顧客センターの「ライセンス配信を表示」では所有権と状態を再検証し、認証情報を URL やログに書き込みません。

カスタム商品の Licentivo identity、device、integration-check API は 409 `CUSTOM_LICENSING_SDK_REQUIRED` を返します。旧 `/credential` は引き続き 410 を返します。アプリは Launch の `ready` 状態を実行許可と見なさず、事業者自身の SDK を使用してください。

## サンプルと受け入れ確認

[Python 薄型アダプターのサンプル](/ja/launch-documents/custom-licensing-example/) は署名、nonce、イベントの重複排除、受領確認の永続化を行う受信フレームワークを提供します。実際のライセンス処理には事業者のシステムとの連携が必要です。

カスタムプロトコルの [OpenAPI](/ja/assets/launch/external-licensing-openapi.json) と Launch 自体の [OpenAPI](/ja/assets/launch/openapi.json) は、それぞれ異なる方向を記述します。受信側は Licentivo の製品カタログ、ポリシーカタログ、または ID 信頼 API を実装する必要はありません。

ローカル検証では隔離された MySQL、実際の HTTP アダプター、模擬ライセンス／決済サービスを使用し、応答の喪失、再起動、更新、アップグレード、返金、アカウント停止、受領確認の不一致、配信の分離を対象とします。本番の受け入れ確認には事業者自身のライセンスサービスとアプリも必要です。宣言されていないデバイス、共有アカウントでのログイン、オフライン機能は Launch が補完しません。
