Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dev-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ jobs:
--set-env-vars="GITHUB_CLIENT_ID=$SANPAI_GH_CLIENT_ID,GITHUB_CLIENT_SECRET=$SANPAI_GH_CLIENT_SECRET"
deploy omikujiGo OmikujiGo \
--trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s \
--set-env-vars="OMIKUJI_COOLDOWN_SECONDS=60,KUDA_BASE_URL=https://kuda.kojiran.workers.dev"
--set-env-vars="OMIKUJI_COOLDOWN_SECONDS=60,KUDA_BASE_URL=https://kuda.kojiran.workers.dev,KUDA_API_KEY=${{ secrets.DEV_KUDA_API_KEY }}"
deploy ogpRewriteGo OgpRewriteGo \
--trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s \
--set-env-vars="FUNC_BASE_URL=$OGP_FUNC_BASE_URL,OGP_PROJECT_ID=d-shrine-dev"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/prod-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ jobs:
--set-env-vars="GITHUB_CLIENT_ID=$SANPAI_GH_CLIENT_ID,GITHUB_CLIENT_SECRET=$SANPAI_GH_CLIENT_SECRET"
deploy omikujiGo OmikujiGo \
--trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s \
--set-env-vars="OMIKUJI_COOLDOWN_SECONDS=28800,KUDA_BASE_URL=https://kuda.kojiran.workers.dev"
--set-env-vars="OMIKUJI_COOLDOWN_SECONDS=28800,KUDA_BASE_URL=https://kuda.kojiran.workers.dev,KUDA_API_KEY=${{ secrets.PROD_KUDA_API_KEY }}"
deploy ogpRewriteGo OgpRewriteGo \
--trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s \
--set-env-vars="FUNC_BASE_URL=$OGP_FUNC_BASE_URL,OGP_PROJECT_ID=d-shrine"
Expand Down
18 changes: 17 additions & 1 deletion app/functions-go/kuda.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ func resolveKudaBaseURL() string {
return "https://kuda.kojiran.workers.dev"
}

// kudaAPIKey は kuda のAPIキー(kuda_...)。環境変数 KUDA_API_KEY で注入し、
// 設定されていれば Authorization: Bearer ヘッダで送る。サーバー間通信なので
// ヘッダレーンのみ使用(半公開キーの ?key= クエリレーンは使わない)。
// 未設定なら無認証で呼ぶ(kudaの移行期間 REQUIRE_API_KEY=0 の互換動作。
// キー必須化後は 401 になり、おみくじは no_entropy として振る舞う)。
var kudaAPIKey = os.Getenv("KUDA_API_KEY")

// kudaClientID は kuda 側の統計・監査ログで消費元を識別するためのラベル。
const kudaClientID = "debug-shrine"

var kudaHTTPClient = &http.Client{}

// kudaFetchTimeout は /drop 一式の取得に許す時間。おみくじは演出中に呼ばれる
Expand Down Expand Up @@ -85,6 +95,10 @@ func fetchKudaDrop(ctx context.Context) (kudaDrop, error) {
return kudaDrop{}, err
}
req.Header.Set("User-Agent", "debug-shrine-omikujiGo")
req.Header.Set("X-Client-Id", kudaClientID)
if kudaAPIKey != "" {
req.Header.Set("Authorization", "Bearer "+kudaAPIKey)
}

resp, err := kudaHTTPClient.Do(req)
if err != nil {
Expand All @@ -96,7 +110,9 @@ func fetchKudaDrop(ctx context.Context) (kudaDrop, error) {
return kudaDrop{}, err
}
if resp.StatusCode != http.StatusOK {
// 503 = プール枯渇。それ以外も「物理乱数が用意できない」として同じ扱い。
// 503 = プール枯渇、429 = 日次クォータ超過、401 = キー無効/必須化後の未認証。
// いずれも「物理乱数が用意できない」として同じ扱い(疑似乱数フォールバック
// はしない)。運用上の区別はこのエラーメッセージのログで行う。
return kudaDrop{}, fmt.Errorf("kuda /drop: status %d: %s", resp.StatusCode, string(body))
}
var drop kudaDrop
Expand Down
54 changes: 53 additions & 1 deletion app/functions-go/kuda_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import (
type kudaMock struct {
Calls int64 // /drop が呼ばれた回数
Depleted bool // true にすると503(プール枯渇)を返す
server *httptest.Server
// LastAuth / LastClientID は直近の /drop リクエストのヘッダ(認証テスト用)。
LastAuth atomic.Value
LastClientID atomic.Value
server *httptest.Server
}

func mockKuda(t *testing.T) *kudaMock {
Expand All @@ -28,6 +31,8 @@ func mockKuda(t *testing.T) *kudaMock {
return
}
n := atomic.AddInt64(&m.Calls, 1)
m.LastAuth.Store(r.Header.Get("Authorization"))
m.LastClientID.Store(r.Header.Get("X-Client-Id"))
if m.Depleted {
w.WriteHeader(http.StatusServiceUnavailable)
fmt.Fprint(w, `{"error":"pool depleted"}`)
Expand Down Expand Up @@ -115,3 +120,50 @@ func TestFetchKudaBytes_Unreachable(t *testing.T) {
t.Error("unreachable kuda should return error")
}
}

func TestFetchKudaDrop_APIKeyHeader(t *testing.T) {
m := mockKuda(t)

// キー設定時: Authorization: Bearer で送る(ヘッダレーンのみ)。
origKey := kudaAPIKey
kudaAPIKey = "kuda_testkey123"
t.Cleanup(func() { kudaAPIKey = origKey })

if _, err := fetchKudaDrop(context.Background()); err != nil {
t.Fatalf("fetchKudaDrop: %v", err)
}
if got := m.LastAuth.Load(); got != "Bearer kuda_testkey123" {
t.Errorf("Authorization = %q, want %q", got, "Bearer kuda_testkey123")
}
if got := m.LastClientID.Load(); got != kudaClientID {
t.Errorf("X-Client-Id = %q, want %q", got, kudaClientID)
}

// キー未設定時: Authorization は送らない(移行期間の無認証互換)。
kudaAPIKey = ""
if _, err := fetchKudaDrop(context.Background()); err != nil {
t.Fatalf("fetchKudaDrop (no key): %v", err)
}
if got := m.LastAuth.Load(); got != "" {
t.Errorf("Authorization should be empty without key, got %q", got)
}
}

func TestFetchKudaDrop_AuthErrorIsNoEntropy(t *testing.T) {
// 401(キー必須化後の未認証)や429(クォータ超過)も503と同様に
// エラー=引けない、として扱う(疑似乱数フォールバックしない)。
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprint(w, `{"error":"invalid key"}`)
}))
orig := kudaBaseURL
kudaBaseURL = server.URL
t.Cleanup(func() {
kudaBaseURL = orig
server.Close()
})

if _, err := fetchKudaDrop(context.Background()); err == nil {
t.Fatal("expected error for 401 response, got nil")
}
}
18 changes: 18 additions & 0 deletions docs/backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -659,3 +659,21 @@ kuda は ANU の量子真空ゆらぎ+ガイガーカウンター(放射性崩
- **フロント**: `Ranking.vue` に「⚔️ せんとうりょく / 🪙 ぽいんと」のピル型
タブを追加。タブ切替は取得済みデータの表示切替のみで再フェッチしない。
「あなたの順位」カード・一覧・単位(bp/pt)が連動する。

## kuda APIキー対応

kudaがAPIキー認証を導入(移行期間中は `REQUIRE_API_KEY=0` でキー無しも可、
フリップ後は401)。おみくじのkudaクライアントを先行対応した。

- **認証**: `KUDA_API_KEY` 環境変数(`kuda_...`)が設定されていれば
`Authorization: Bearer` ヘッダで送る。サーバー間通信なのでヘッダレーンのみ
使用(半公開キー `kudaq_` の `?key=` クエリレーンは使わない)。
併せて `X-Client-Id: debug-shrine` を送り、kuda側の統計・監査で識別可能にする。
- **キーの注入**: GitHub Secrets `DEV_KUDA_API_KEY` / `PROD_KUDA_API_KEY` →
デプロイworkflowの omikujiGo `--set-env-vars`。Secret未設定なら空=無認証の
互換動作のまま。
- **エラー方針は不変**: 401(キー無効)・429(日次クォータ超過)も503(枯渇)と
同じ「引けない」(no_entropy)。疑似乱数フォールバックはしない。
- **クォータ注意**: kudaの新規キー既定は30滴/日。おみくじは3バイト/回なので
既定のままだと1日10回で頭打ちになる。キー発行時(ダッシュボード・Nostr認証)に
daily_quota を用途に合わせて引き上げること。
Loading