mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
* feat(entitlement): gate multi-agent to Plus tier via signed entitlement token Multi-agent fan-out was gated on the binary isPlusUser flag, which any valid license flips true, so a future Lite tier would wrongly pass. Introduce a server-signed entitlement token (ES256, verified offline via WebCrypto) as the single entitlement primitive. - Rename the broad flag isPlusUser to isPaidUser (any paid license, incl. Lite); all existing Plus-feature gates move to it, preserving current behavior. - Add a new strict isPlusUser (tier >= Plus) derived from the token's multi_agent feature. The multi-agent UI gate and the authoritative send-boundary check (ensureMultiAgentEntitlement) now key on it. - No-token fallback (isPlusUser = isPaidUser) preserves today's behavior until the server ships tokens alongside Lite/Pro, so there is no plan-name list in the public client to patch out. - Settings sanitize migration backfills isPaidUser from the legacy isPlusUser. - src/entitlement/ verifies the JWS signature with an embedded public key (no key can mint tokens client-side); forged data.json / faked responses fail. Tests cover tier gating across Free/Lite/Plus/Pro plus the crypto layer (valid, expired, wrong-user, tampered, unknown-kid, malformed). Self-host migration to the token is deferred to logancyang/obsidian-copilot-preview#201. Closes logancyang/obsidian-copilot-preview#200. Design: "Copilot Entitlement Token Design". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(test): swap in Node WebCrypto when jsdom's subtle lacks generateKey CI (Node 22) ships a jsdom whose window.crypto.subtle exists but is partial (no generateKey), so the previous `!subtle` guard skipped the polyfill and the entitlement crypto tests failed. Probe for the actual method and install Node's complete WebCrypto when it's missing. Runtime is unaffected (test-only setup). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(entitlement): never downgrade on an unverifiable token (Codex P1/P2) Addresses two Codex findings: - P1: when /license starts returning a token but the client's public keys are not shipped yet (or during kid rotation), every token verified as null and applyEntitlement cleared the flags, dropping paying users to free. Now applyEntitlement never clears: it returns false when it cannot verify, and validateLicenseKey falls back to turnOnPaid() for a license the server already confirmed valid. Only an authoritative negative (invalid license / no key) downgrades, via turnOffPaid. - P2: removed refreshEntitlementFromCache() and its concurrent startup call, which could clear flags after checkIsPaidUser() restored them. The persisted flags already hold the last verdict and the online check is authoritative; offline expiry enforcement belongs to the deferred self-host migration (#201). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(test): run entitlement crypto test in node env, not flaky jsdom subtle jsdom ships only a partial SubtleCrypto (no generateKey), and patching it in jest.setup was nondeterministic across CI Node builds (passed on one commit, failed on the next with no setup change). Run verify.test under the node environment, where crypto.subtle is Node's complete WebCrypto — the verification logic is WebCrypto-spec behavior, identical between Node and the webview. Also drop verify.ts's @/logger import so the entitlement module is a pure leaf (no settings/obsidian graph), which keeps the node-env test import-safe and matches the module's intended dependency-light design. jest.setup now guards its window usage so it is a no-op under the node environment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(test): force WebCrypto onto globalThis, not just window The prior fix patched window.crypto, but a bare `crypto` reference in a module resolves to globalThis.crypto, and jest's jsdom environment installs a partial SubtleCrypto (no generateKey) on globalThis that the window-only patch missed — so CI kept failing in jsdom while local (clean jsdom) passed. Patch globalThis (and window) with Node's complete WebCrypto, with a fallback to replacing `.subtle` if `crypto` is a locked accessor. Reverts the node-env docblock on the crypto test; the global patch covers the standard jsdom env. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(test): inject Node WebCrypto into entitlement verify instead of patching globals Root cause of the repeated CI failures: jest's jsdom environment exposes a partial, locked SubtleCrypto (no generateKey/ECDSA) on the global the module resolves `crypto` from, and neither replacing window/globalThis.crypto nor the @jest-environment node docblock reliably overrode it on CI (local jsdom resolves clean, so it couldn't be reproduced locally). Fix: give verifyEntitlement an injectable `subtle` option (default `crypto.subtle`, as used at runtime on desktop + mobile), and have the test pass Node's webcrypto.subtle explicitly. The test no longer depends on the environment's global WebCrypto at all, so it's deterministic in any jest env. jest.setup reverted to its original form (no crypto hacks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(entitlement): close 3 Codex findings (Lite bypass, log leak, offline expiry) - P1 (Lite bypass): an unverifiable entitlement token no longer grants the strict gate. validateLicenseKey now calls markPaidPendingEntitlement() — paid so general Plus features work, but isPlusUser stays false so an unverifiable Lite token can't pass the multi-agent gate before the verifying key ships (fails closed). Tokenless (old server) still grants paid + Plus. - P2 (log leak): parseBrevilabsResponse redacts the `entitlement` JWS before logInfo, so it never lands in the shared copilot-log.md. - P2 (offline expiry): persist the token's exp as entitlementExpiresAt and have the strict gate (isPlusEnabled / useIsPlusUser) honor it, so multi-agent locks at expiry even while /license is unavailable. The broad paid gate keeps legacy behavior. ensureMultiAgentEntitlement's slow path re-reads it, so an expired token offline is blocked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * feat(entitlement): embed prod ES256 public key (kid ent-2026-06) Populate ENTITLEMENT_PUBLIC_KEYS with the production verify-only public JWK so verifyEntitlement can validate server-signed tokens offline. The matching private key signs tokens in brevilabs-api. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X * fix(entitlement): require in-session signature proof for strict Plus Address Codex P1: isPlusEnabled() trusted the persisted isPlusUser boolean + entitlementExpiresAt without re-verifying the JWS, so an edited data.json (isPlusUser: true + future expiry) bypassed the multi-agent gate offline and in the startup window. The signed token was not actually the gate on cached state. Gate token-derived strict Plus on an in-memory, non-persisted proof that a signed entitlement granting multi_agent was cryptographically verified in THIS process — set by applyEntitlement (server response) or the new verifyCachedEntitlement (cached token re-checked at startup, offline via WebCrypto). A persisted boolean alone never sets it, so it fails closed. The tokenless fallback (pre-token server, entitlementExpiresAt === 0) is unchanged: no signed artifact exists to verify, so it honors the license-confirmed flag as today and preserves new-plugin/old-server compatibility. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T4qVU9iszFGZ1q5ro8m2X --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| backends | ||
| catalog | ||
| chatModel | ||
| models | ||
| providers | ||
| setup | ||
| state | ||
| types | ||
| ui | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| createModelManagement.test.ts | ||
| createModelManagement.ts | ||
| index.ts | ||