Auth Denied
plasma における認証失敗には、3 つの異なる種類があります。どれに当たっているかで 対処が決まります。
種類 1 — リクエストレベルの 401
Section titled “種類 1 — リクエストレベルの 401”症状:
/sync/pushまたは/sync/pullで HTTP 401- リトライ枯渇後に
client.onErrorが{ kind: "push-http", status: 401, url }または{ kind: "pull-http", status: 401, url }で発火する
原因: SyncHandlerOptions.auth(req) が { ok: false } を返しました。
対処: auth 関数が何をしているか確認します。
auth: async (req) => { const token = req.headers.get("authorization")?.replace("Bearer ", "") console.log("auth token:", token?.slice(0, 8)) // sanity log
if (!token) return { ok: false, reason: "no token" }
const user = await verifyJWT(token) if (!user) return { ok: false, reason: "bad token" }
return { ok: true, clientGroupID: user.id, clientID: req.headers.get("x-client") ?? "unknown", ctx: { userId: user.id }, }}よくある原因:
- クライアントが
Authorizationヘッダーを送っていない。PlasmaClientOptions.authHeadersが配線されているか確認してください。createPlasmaClient({...,authHeaders: async () => ({"authorization": `Bearer ${await getToken()}`,}),}) - トークンがセッション途中で期限切れになった。
authHeadersはすべての リクエストで呼ばれるので、積極的にリフレッシュするトークン関数が対処になります。
種類 2 — mutator レベルの PlasmaAuthorizationError
Section titled “種類 2 — mutator レベルの PlasmaAuthorizationError”症状:
- push は
200 OKを返した (ワイヤ上で{ ok: true })。 SyncClientErrorは発火しない。- ユーザーの optimistic apply は一瞬成功したように見えたが、次の pull で 行が静かに消えた。
原因: あるテーブルの auth.write(ctx, row) が false を返しました。
サーバー側の mutator が PlasmaAuthorizationError を throw し、トランザクションが
ロールバックされ、last_mutation_id は依然として進み (poison がリトライされない
ように)、change log 行は書かれませんでした。次の pull でクライアントの
dropConfirmed が outbox エントリを削除し、rebuildOptimistic が (その書き込みを
見ていない) base ストアの上に生き残った outbox をリプレイし、optimistic ビューが
revert します。
対処: テーブルの auth.write predicate を確認します。よくある原因:
- サーバー上の
ctxの値が、期待したフィールドを持っていない。auth()がctx: { userId: user.id }を返すのに、行のuserIdが異なる形式 (email か UUID か) を使っている場合、predicate は決して一致しません。 - クライアントが、現在のユーザーに属さない
userIdを持つ行を optimistic に 書き込んだ — おそらくユーザー切り替え前の古いフォームの値です。 - 更新している行が別のユーザーによって作成されたもので、UI が「編集」ボタンを 所有権でゲートしていなかった。
UI での応答: mutate() を呼ぶ前にコンポーネントで権限を事前チェックします。
クライアントはサーバー側の PlasmaAuthorizationError を直接観測できないため、
目に見える失敗モードは「行が戻ってきた」ことです。所有権を反映するように
ボタンの状態を設計してください。
<button disabled={row.userId !== ctx.userId} onClick={() => update.mutate({ ... })}> Edit</button>種類 3 — pull で行が欠ける
Section titled “種類 3 — pull で行が欠ける”症状:
- エラーイベントは一切発火しない。
- しかしユーザーが見えることを期待している行が、決して現れない。
- Devtools パネルは、サーバー上の
_plasma_changesにはそれらがあると表示するが、 クライアントの IDB にはない。
原因: テーブルの auth.read(ctx, row) が、pull 時にそれらの行に対して
false を返しました。plasma は認可されていない行を pull レスポンスから静かに
フィルタリングし、ユーザーが許可されていない行を決して見ないようにします。
対処: 種類 2 と同じ predicate の診断です。確認事項:
- pull 時に
ctx.userId(または predicate が使うフィールド) が期待どおりか?auth()ハンドラでログを取ってください。 - 行自身のフィールド (
row.userId) が期待どおりか? サーバー DB を直接 クエリします。Terminal window wrangler d1 execute my-app-prod --command "SELECT id, userId FROM todos WHERE ..." - 集合メンバーシップのつもりで、うっかり厳密等価を使っていないか?
(ユーザーが多くのチームのメンバーなのに
row.teamId === ctx.userIdとしている等)。
wrangler tail でのデバッグ
Section titled “wrangler tail でのデバッグ”すべての auth 判断は、まずあなたの auth() 関数を通ります。そこでログを取り、
tail を監視します。
auth: async (req) => { const result = await realAuthFunction(req) console.log("auth:", req.url, result.ok, result.reason) return result}pnpm wrangler tailすべてのリクエストが 1 行ログを出します。クライアントの失敗した mutation を サーバーの拒否と相関付けられます。
- UX が有利になる箇所ではクライアントで事前チェックする。 よくあるケースで revert に頼らないでください。
- 現実的な ctx 値でテストする。
auth.writeがfalseを返すケースを 行使し、トランザクションがロールバックされ (change log に一致する行がない) 、last_mutation_idが poison を落とすために進んだことをアサートする認証テストを、 サーバー側のテストスイートに含めてください。 - 拒否を集計してログに残す。 本番では
auth()の戻り値をテレメトリに hook し、パターンを見つけられるようにしてください (「バージョン X のユーザーが 全員 401 になっている」— おそらくトークン形式の変更)。
次に読むもの
Section titled “次に読むもの”- 認証と権限 — 認証の 3 つのレベル
- Sync errors — エラー kind 表の全体