「X 抽選キャンペーンの当選者だけが LINE に登録できる、流入元タグ付き連携を作りたい。4日くらいかかる?

最初の見積もりはそう答えました。X 側で一意トークンを発行し、DM の LP リンクに付与し、LP で受け取って LINE に渡し、LINE 側で X user_id に解決して friend_diagnosis に保存する。各ステップの実装で1日ずつ、合計4日。

実際は 半日で本番稼働 しました。鍵は OSS コードリーディング でした。

当初の見積もりと実態の差

実装ボリュームを最初に見積もったときの内訳と、実際の差は次の通りです。

レイヤー当初見積実態ギャップの理由
X Harness: 当選DMに一意token付与半日〜1日既に実装済みOSS が appendRef(gate.link, 'xh:' + delivery.token) を engagement-gate.ts:152 で既に実装
X Harness: token→ X user_id 検索API1日既に実装済みOSS の GET /api/tokens/:token/resolve がすでに公開エンドポイントとして稼働
LP: ?ref=xh: 抽出半日🟡 5-10行追加で完了URL パラメータ解析 + JSON 同梱だけ
LINE Harness: lp-hook.ts の ref処理数時間🟡 30-50行追加で完了既存 decodeSource を拡張 + 新ヘルパ関数
LINE Harness: friend_diagnosis 拡張数時間🟡 migration 111 で4カラム追加ALTER TABLE 1ファイル
X↔LINE webhook1日不要OSS resolveToken() が atomic consume するため webhook 不要
合計3-4日約半日OSS既存実装の発見が時短の本体

OSS のコードを読まずに自前で書き始めると、4日かかった可能性が高い実装でした。読むだけで時間が10分の1になりました。

設計の核 — atomic resolveToken() を発見

OSS(x-harness-oss)の packages/db/src/engagement-gates.ts:256 に、こんな関数がすでにありました。

``typescript
export async function resolveToken(db: D1Database, token: string): Promise {
// Atomically mark the token as consumed and return its data in a single statement.
// If consumed_at is already set (or token doesn't exist), the UPDATE matches zero rows
// and we return null — preventing double-redemption races.
const now = jstNow();
const row = await db
.prepare('UPDATE engagement_gate_deliveries SET consumed_at = ? WHERE token = ? AND consumed_at IS NULL RETURNING x_user_id, x_username, gate_id')
.bind(now, token)
.first<{ x_user_id: string; x_username: string | null; gate_id: string }>();
if (!row) return null;

// ... LINE Harness config を取得して返す
}
`

そして apps/worker/src/routes/engagement-gates.ts:120-125 には、auth 不要の public endpoint として公開されていました。

`typescript
// Public endpoint — no auth required (token is the secret)
engagementGates.get('/api/tokens/:token/resolve', async (c) => {
const result = await resolveToken(c.env.DB, c.req.param('token'));
if (!result) return c.json({ success: false, error: 'Token invalid or already consumed' }, 404);
return c.json({ success: true, data: result });
});
`

UPDATE...RETURNING` 句で atomic に consume + return を1コールで完結 しています。race condition も発生しません。token 自体が64文字のランダム秘密のため、auth は不要という設計判断も合理的です。

私が当初書こうとしていた「verify-token」「claim-token」の2エンドポイント分割は、この1関数で全部置き換えられます。

X-LINE 真の連携フロー全体図
X-LINE 真の連携フロー全体図

💡 KEY TAKEAWAYS
SaaS 連携実装で「自前で書く」前に OSS の README、SPEC、エンドポイント一覧を読む こと。既存機能の存在を見落として車輪の再発明をすると、工数が10倍違います。

OSS活用で実装工数を10分の1にする組織体力、御社にあるか3分で診断しませんか?
細マッチョ企業診断 / 3 分 8 問
診断する

4つの落とし穴 — 実装中につまずいた場所

実装中に4箇所で立ち止まりました。それぞれの原因と突破方法を共有します。

落とし穴 1: Permission Read-only で 403 連発

実装が一通り終わって動作確認を始めたら、X API がすべて 403 で弾かれました。

``json
{
"title": "Forbidden",
"status": 403,
"detail": "Your client app is not configured with the appropriate oauth1 app permissions for this endpoint."
}
`

最初は OSS のコードを疑い、X Harness Worker の routes/posts.ts を読み返しました。問題なし。次に の x_accounts テーブルを確認。トークンも揃っている。

原因は X Developer Portal のアプリ権限が Read-only のままだったことでした。アクセストークンが発行された当時は Read で足りていて、後から Write/DM 機能を追加実装したのに、X Developer Portal のアプリ権限を変更し忘れていました。

  1. X Developer Portal → 該当アプリ → Settings → User authentication settings → Edit
  2. App permissions を 「Read and write and Direct message」 に変更
  3. Keys and tokens タブ → Access Token を再生成(権限変更後は旧トークン無効)
  4. D1 の x_accounts テーブルに新トークンを UPDATE

`bash
npx wrangler d1 execute kei-soulmate-x --remote --command \
"UPDATE x_accounts SET access_token='$NEW_T', access_token_secret='$NEW_S' WHERE id='...';"
`

教訓: SaaS 連携の「動かない」の8割は権限・スコープ・トークン。コードを掘る前に、まず Developer Portal でアプリ権限と最新トークンを確認すべきです。

落とし穴 2: PowerShell で bash の curl は動かない

検証用の curl コマンドを PowerShell に貼ったところ、3回連続でエラーになりました。

`text
curl : ドライブが見つかりません。名前 'https' のドライブが存在しません。
`

PowerShell では curlInvoke-WebRequest のエイリアスになっており、-s フラグが drive として解釈されています。bash の curl とは構文が完全に別物です。

突破: 3つの解決策があります。

`powershell

方法A: curl.exe を明示


curl.exe -s "https://api.example.com" -H "Authorization: Bearer xxx"

方法B: PowerShell ネイティブ

$h = @{ "Authorization" = "Bearer xxx" } Invoke-RestMethod -Uri "https://api.example.com" -Headers $h

方法C: git-bash で実行

bash の構文がそのまま動く

`

最終的にチーム内で混在させないため、check.ps1 という PowerShell スクリプト1本 を置いて、検証時はそれを実行する規約にしました。Invoke-RestMethod ベースで書かれているため、PowerShell から .\check.ps1 で1行実行できます。

教訓: シェル混在環境ではコマンドを書くより、実行ヘルパーを置く方が生産性が高い

落とし穴 3: がクロスプラットフォームで破壊される

LINE Harness をデプロイしようとしたら、ローカルの wrangler が起動しませんでした。

`text
Error: Cannot find module '.../node_modules/wrangler/bin/wrangler.js'
MODULE_NOT_FOUND
`

原因は Mac で pnpm install した node_modules を 経由で Windows に同期したことでした。node_modules/.bin/wrangler のシンボリックリンクは存在しているのに、本体の node_modules/wrangler/ ディレクトリが部分的に欠損していました。

pnpm/npm の node_modules には OS 固有のバイナリ(workerd、、native bindings)が含まれており、クロスプラットフォーム同期で壊れます。

突破: 3つの解決策があります。

`bash

方法A: Nextcloud で node_modules を同期除外


.nextcloudignore に node_modules/, .next/, dist/, .wrangler/ を追加

方法B: Win 側で pnpm install --force を再実行

cd line-harness-oss && pnpm install --force

方法C(採用): npx -y wrangler@4 で都度ダウンロード

npx -y wrangler@4 d1 execute line-harness --remote --file=migration.sql
`

最終的に方法C を採用しました。バージョン固定で安定し、node_modules の破壊問題から永久に解放されます。

教訓: クロスプラットフォーム同期するプロジェクトでは、node_modules を同期しない。npx の都度ダウンロードが最も安定。

落とし穴 4: bash の curl で日本語 JSON 送信が 文字化け

ゲートの replyKeyword を「鑑定希望」から「運命」に更新するために、curl で PUT リクエストを送ったら、こうなりました。

`json
"replyKeyword": "\udcefソス^\udcefソス\udcefソス"
`

bash のコマンドライン引数で渡した日本語が、Shift-JIS と UTF-8 の混在環境で破壊されました。

突破: JSON を で生成してファイルに書き出し、curl の --data-binary @file で送信する。

`bash
python <<'EOF' > /tmp/gate_update.json
import json, sys
sys.stdout.buffer.write(json.dumps({"replyKeyword": "運命"}, ensure_ascii=False).encode("utf-8"))
EOF

curl -s -X PUT "$WORKER/api/engagement-gates/$GATE" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json; charset=utf-8" \
--data-binary @/tmp/gate_update.json
`

教訓: 日本語を含む JSON は Python 経由で生成、ファイルから送信が安全。シェル引数の文字化けは環境差で再現性が低く、デバッグ時間を奪います。

4つの落とし穴と突破方法
4つの落とし穴と突破方法

学び — OSS活用の組織体力

この実装から得た学びは3つです。

1. 自前実装の前に OSS の README / SPEC / エンドポイント一覧を読む。
コードリーディングは「面倒な前置き」ではなく、最も投資効率の高い時間です。当方は今回30分の OSS リーディングで4日を半日に圧縮しました。

2. _custom/ パターンで OSS のコアコードに触れない規約を持つ。
upstream 追従コストを最小化できます。今回も最初に書いた _custom/deliveries.ts を OSS 機能と重複と判明した時点で削除し、コア未編集ルールを完全準拠で運用継続しました。

3. SaaS 連携の「動かない」の8割は権限・スコープ・トークン。
コードを疑う前に Developer Portal のアプリ権限と最新トークンを確認する。OAuth Permission の罠は誰もが踏みます。

当方の OSS 運用ルール(参考)

`markdown

X / LINE Harness OSS 運用ルール

コア未編集の原則

- packages/db/, apps/worker/src/routes/ は触らない - 独自機能は apps/worker/src/_custom/ 配下に追加 - カスタムDB拡張は _custom/db/migrations/<番号>_<名前>.sql

upstream 追従

- 月1回 git fetch upstream && git merge upstream/main - conflict が発生しないように _custom/ 以外を編集しない

例外的にコアに触れる場合

- MODIFICATIONS.md に経緯と差分を記載 - 次回 upstream マージ時の手順を明記

デプロイ前チェック

- [ ] _custom/ 配下のみ変更されているか - [ ] migration 番号が連番になっているか - [ ] secrets が D1 ではなく Worker secrets に入っているか
``

次のアクション

OSS をローカル fork で運用するか、自前で1から書くか、SaaS をフルマネージドで使うかの判断は、組織の OSS活用体力 によって変わります。当方は + D1 + 自家 fork OSS の組み合わせを軸にしていますが、これは一定のコードリーディング体力を前提にしています。

御社の組織が OSS を活用できる体質か、SaaS 中心で行くべきか、3分でセルフチェックしてみたい方は 細マッチョ企業診断 をご利用ください。当方が今回の実装経験から組み立てた診断項目を反映しています。