# レシピ：ライセンスキー

> デスクトップアプリ、CLI、プラグイン、テンプレートをライセンスキー付きで販売。アプリは内部にシークレットを一切持たずにキーを検証します。

Perly Pay は支払いが完了するとキーを発行し、端末ごとのアクティベーションを数え、公開エンドポイントで「このキーはここでこの商品に有効か？」に答えます。配布するプログラムは `LicenseClient` を使い、シークレットキーは必要ありません。

> **注:** アシスタントと一緒に：このレシピは、Perly Pay プラグインの `sell-license-keys` スキルがあなたのプロジェクトで実行します。動くサンプルは、プラグインのリポジトリの `examples/license-keys`（MIT）です。

## 仕組み

1. **キーを発行する商品** — `issues_license: true` と `license_activation_limit`（キーあたりの端末数）、一回払い・買い切り・期間制の価格、またはサブスクリプションで。
2. **販売** — サイトに[支払いリンク](https://pay.perly.io/docs/payment-links.md)、または購入者ごとに請求書。
3. **キーを送る** — Webhook `license.created`（オブジェクトがキー）で、または `invoice.fulfilled` の後に `licenseKeys.list({ customer })` で。領収書にキーは載りません。
4. **アプリでアクティベート** — その端末で変わらない名前で `activate({ key, product, instance_name })`。アクティベーション ID を保存します。
5. **起動時に検証** — `validate({ key, product, instance })`。明確な `valid: false` で閉じます。オフラインの間は、最後の正常な応答が数日有効です。

**Node**

```js
import { LicenseClient } from "perly-pay";

const licenses = new LicenseClient(); // no secret key, ever
const result = await licenses.activate({ key, product: "prod_…", instance_name: deviceName });
if (!result.valid) showWhy(result.reason); // limit_reached, revoked, expired, …
```

**PHP**

```php
use Perly\Pay\LicenseClient;

$result = (new LicenseClient())->activate(['key' => $key, 'product' => 'prod_…', 'instance_name' => home_url()]);
if (!$result['valid']) echo $result['reason'];
```

## サンプルを動かす

1. **インストール** — `examples/license-keys` で `npm install`（Node 20.12+）。
2. **キー** — `.env.example` を `.env` にコピーし、`PERLY_PAY_SECRET_KEY` にテストキー（`sk_test_…`）を書きます。
3. **準備** — `npm run setup` があなたのキーで商品と価格を作成し、`.env` 用にその ID を表示します。
4. **起動** — README のとおり `npm start`（または `npm run dev`）。
5. **ウォレットなしで支払う** — `npm run buy-test -- you@example.com` がテストモードで 1 本購入してキーを表示します。続いて `npm run app -- activate <key>` と `npm run app -- pro hello`。

## 落とし穴

- アプリの中の `sk_…` キー。誰でも取り出せます。配布するプログラムは `LicenseClient` だけを使います。
- `product` なしの呼び出しは拒否されます（`parameter_missing`）。毎回渡してください。ほかの商品のキーは `wrong_product` になります。
- 起動のたびに変わる `instance_name`：起動ごとにアクティベーションを消費します。
