最小サンプル
ここに挙げるサンプルは意図的に小さく作ってあります。まだ SDK ではなく、連携を パッケージ化する前に「動くこと」を確かめるためのコピー & ペースト用のひな形です。
シークレットはチャット、ソース管理、ブラウザーのログ、サーバーログ、課題 トラッカーに残さないでください。
client_idは公開情報ですが、client_secret、 アクセストークン、リフレッシュトークン、Webhook のシークレットは違います。
環境変数
コミットしないローカルの .env を作成します。
EVOMAP_BASE_URL=https://evomap.ai
EVOMAP_CLIENT_ID=evm_client_live_or_test_...
EVOMAP_CLIENT_SECRET=keep-this-local
EVOMAP_REDIRECT_URI=http://localhost:3000/callback
EVOMAP_SCOPE=recipe:read
公開の実験では、テストモードのクライアント(その公開は実際のバリュープールに届きません)を使い、次を申請してください。
EVOMAP_SCOPE="recipe:read recipe:write recipe:publish"
Node: OAuth と最初の API 呼び出し
インストール:
npm init -y
npm install express dotenv
server.mjs:
import crypto from "node:crypto";
import express from "express";
import "dotenv/config";
const app = express();
const base = process.env.EVOMAP_BASE_URL || "https://evomap.ai";
const redirectUri = process.env.EVOMAP_REDIRECT_URI;
let pending = null;
function makePkce() {
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
return { verifier, challenge };
}
app.get("/login", (_req, res) => {
const { verifier, challenge } = makePkce();
const state = crypto.randomBytes(16).toString("base64url");
pending = { verifier, state };
const url = new URL(`${base}/oauth/authorize`);
url.searchParams.set("response_type", "code");
url.searchParams.set("client_id", process.env.EVOMAP_CLIENT_ID);
url.searchParams.set("redirect_uri", redirectUri);
url.searchParams.set("scope", process.env.EVOMAP_SCOPE || "recipe:read");
url.searchParams.set("code_challenge", challenge);
url.searchParams.set("code_challenge_method", "S256");
url.searchParams.set("state", state);
res.redirect(url.toString());
});
app.get("/callback", async (req, res) => {
if (!pending || req.query.state !== pending.state) return res.status(400).send("bad state");
const tokenRes = await fetch(`${base}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: String(req.query.code || ""),
client_id: process.env.EVOMAP_CLIENT_ID,
client_secret: process.env.EVOMAP_CLIENT_SECRET,
redirect_uri: redirectUri,
code_verifier: pending.verifier,
}),
});
if (!tokenRes.ok) return res.status(tokenRes.status).send(await tokenRes.text());
const tokens = await tokenRes.json();
const apiRes = await fetch(`${base}/developer/oauth/recipes?limit=5`, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
res.type("json").send(await apiRes.text());
});
app.listen(3000, () => console.log("Open http://localhost:3000/login"));
実行:
node server.mjs
Python: OAuth のトークン交換とカタログの読み取り
インストール:
python -m venv .venv
. .venv/bin/activate
pip install requests python-dotenv
read_recipes.py は、Web アプリ側でコールバックの code と元の PKCE verifier を
すでに取得している前提です。
import os
import requests
from dotenv import load_dotenv
load_dotenv()
base = os.getenv("EVOMAP_BASE_URL", "https://evomap.ai")
code = os.environ["EVOMAP_CODE"]
verifier = os.environ["EVOMAP_CODE_VERIFIER"]
r = requests.post(f"{base}/oauth/token", data={
"grant_type": "authorization_code",
"code": code,
"client_id": os.environ["EVOMAP_CLIENT_ID"],
"client_secret": os.environ["EVOMAP_CLIENT_SECRET"],
"redirect_uri": os.environ["EVOMAP_REDIRECT_URI"],
"code_verifier": verifier,
}, timeout=20)
r.raise_for_status()
access_token = r.json()["access_token"]
recipes = requests.get(
f"{base}/developer/oauth/recipes",
params={"limit": 5},
headers={"Authorization": f"Bearer {access_token}"},
timeout=20,
)
recipes.raise_for_status()
print(recipes.json())
テスト公開の形
まずテストモードを使ってください。書き込み呼び出しには Idempotency-Key を
付けます。
curl -X POST "$EVOMAP_BASE_URL/developer/oauth/recipe/publish" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: local-test-001" \
--data @recipe.json
テスト用認証情報の場合、レスポンスには livemode: false が含まれるはずです。
同じべき等キーを異なるボディで使い回すと、EvoMap は競合を返します。
Webhook の検証
サーバーは、ペイロードをパースしたり信頼したりする前に、生のリクエストボディを
検証しなければなりません。現行のヘッダーは
X-EvoMap-Webhook-Signature: t=<unix>,v1=<hex> です。
import crypto from "node:crypto";
export function verifyEvoMapWebhook(rawBody, signatureHeader, secret) {
const fields = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
const timestamp = Number(fields.t);
const signature = fields.v1;
if (!timestamp || !signature) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const actual = Buffer.from(signature || "", "hex");
const wanted = Buffer.from(expected, "hex");
return actual.length === wanted.length && crypto.timingSafeEqual(actual, wanted);
}
生成クライアントのひな形
公式 SDK が出るまでは、ライブの OpenAPI 仕様から型付きクライアントを生成して ください。
curl -fsS https://evomap.ai/openapi.json -o openapi.json
npx openapi-typescript openapi.json -o evomap-api.d.ts
生成されたコードは手で編集せず、CI で生成する運用にしてください。本番ビルドでは OpenAPI のバージョンかコミットハッシュを固定します。
次に進めたい堅牢化
- PKCE の verifier と
stateはブラウザーセッションごとに永続化してください。 - リフレッシュトークンは保存時に暗号化してください。
invalid_grantやリフレッシュトークンの再利用検出ではリトライループを止め、 再ログインを強制してください。- 429 と一時的な 5xx レスポンスには指数バックオフを使ってください。
- パブリッククライアントはシークレットを保持できないものとして扱い、 コンフィデンシャル限定のエンドポイント(トークンイントロスペクションなど)を パブリッククライアントから呼ばないでください。
- リクエスト id、ステータス、エンドポイント、レイテンシーをログに残してください —— トークンのボディやシークレットは絶対に残さないでください。