Repository navigation
feat: offline licenses authenticate the CLI without the platform - #2127
ThomasRooney wants to merge 2 commits into
Conversation
|
Running ultrareview automatically — This PR introduces offline license authentication, altering core auth, license resolution, and config persistence across 13 files; a subtle bug could break CLI auth or permit unauthorized generation.. I'll post findings when complete. |
There was a problem hiding this comment.
Ultrareview completed in 11m 37s
All reported issues were addressed across 13 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
571ca9d to
07d00de
Compare
There was a problem hiding this comment.
All reported issues were addressed across 15 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
07d00de to
5fa3b8d
Compare
There was a problem hiding this comment.
All reported issues were addressed across 13 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
a63f30c to
30cd2ed
Compare
93d1529 to
2cd9b57
Compare
e299702 to
7f21479
Compare
… commercial license (#2124) ## Why - The platform issues target-scoped license tokens (speakeasy-registry#4708, live) and the generator validates them offline (openapi-generation#55); the CLI has to attach them to every generation. - The CLI generates only under the customer's commercial license. The AGPL election is a source-build fallback in the upstream generator and is not used by the CLI. - On `main`, the paid-tier "not in the approved SDK target list" warn path has produced AGPL-stamped output since the generation-context change. ## What Changed Dependencies: `openapi-generation/v2` v2.933.1 → v2.934.0 (offline license validation with target-scoped tokens), `generation-context` v1.0.0 → v1.1.0 (`WithDirect` no longer implies AGPL; the election is explicit). Uses `speakeasy-core` v0.24.0 (`access.CheckGenerationAccess` exposing `AccessDetails.license_jwt`) and `speakeasy-client-sdk-go/v3` v3.28.1, already on `main`. - Every generation elects commercial (`internal/sdkgen.withGenerationContext`) and attaches the authenticated context's license token; the generator validates signature, expiry, and target coverage offline. `/v1/auth/validate` issues the token for every tier (speakeasy-api/speakeasy-registry#4709: commercial `["*"]` at 30d, free `["*"]` at 24h), so the context token is authoritative and the access check's token is not consulted. - No token: the election is still commercial and the generator rejects the run as an unproven commercial election. - Blocked access (`Level == blocked`) aborts before generation, unchanged. - Result vs `main`: the paid-tier warn path produces commercial output again. Free workspaces get commercial output for their one language; a second language is blocked by the access check. - `lint --dry-run` elects commercial with the context's token. A dry run that fails before producing any warnings (unauthenticated, or a rejected election) skips that target, and the summary names the skipped targets instead of reporting "no warnings found". - Standalone `generate codeSamples` and `generate usage` — dead on `main` (`ErrMissingGenerationAccess`, no generation state ever established) — now elect through the same helper: they work when authenticated and fail with a clear "unauthenticated" error otherwise. - `internal/run` performs no election of its own (the workflow-level AGPL/direct election is removed); election happens in `internal/sdkgen`. Validation elects authenticated commercial for authenticated callers (keeping SDK access, e.g. link shortening) and direct OSS mode otherwise; it attaches no license token, since it produces no licensed output and an invalid token must not fail read-only diagnostics. **Rollout prerequisite:** the deployed platform must issue `license_jwt` from `/v1/auth/validate` for every allowed tier before this ships — a platform that authenticates without issuing a token now produces a hard generator rejection (unproven commercial election) where `main` fell back to AGPL. Hosted prod satisfies this as of speakeasy-api/speakeasy-registry#4709 (deployed); older self-hosted/staging registries are the population at risk. Follow-up: #2127 adds offline license authentication on top of this. ## Testing - `go build ./...`; unit suite (excluding `integration`) green; golangci-lint clean on touched packages. - Unit coverage: election (`internal/sdkgen/generation_context_test.go`). - Prod smoke in a sandboxed `$HOME` during development: speakeasy-self `run` → commercial output, no warning. Free workspace `run -t go` → commercial output; `run -t typescript` → "Upgrade Required … exceeded the limit of one free generated SDK", blocked.
Adds offline license support: a stored license token (SPEAKEASY_LICENSE_TOKEN, SPEAKEASY_LICENSE_FILE, or offline_license_token in the CLI config) authenticates offline-capable commands (run, generate sdk) without calling /v1/auth/validate. With a configured API key the context keeps an SDK client so registry uploads, telemetry, and workspace confirmation behave as online; without one the run is fully offline. Online re-authentication happens when the license does not cover a selected target (EnsureTargets) and when a platform API needs an SDK client — GitHub workflow runs and workflows that hard-require the registry: frozen lockfile runs, registry inputs or overlays, and blocking code-samples registry output (EnsurePlatform). Best-effort registry interactions still skip offline. Headless sessions without an API key fail fast with the offline-license hint instead of opening a browser. Generation access for an offline-license context is decided by the signed token (expiry and target coverage, validated by the generator) instead of the platform access check. The offline license is ignored when an API key is configured but its workspace is not yet known, so an env token cannot bind a different workspace's identity to the key. Successful online authentication persists the issued token (non-free tier, matching workspace) — except inside GitHub Actions, where a persisted token would flip the same job's later commands to offline auth and bypass the platform access check. Tokenless re-auth in the same workspace preserves the stored token; switching workspaces clears it with a notice; logging out clears it. Authentication into the speakeasy-self default workspace remains a no-op, preserving the stored customer id.
The offline-context SDK client keeps the SDK's default bounded HTTP client, matching the client core auth stores after online authentication. An env-supplied license with an unknown API-key workspace is honored with a warning instead of forcing an online round-trip air-gapped environments cannot make; only config-stored licenses require the workspace to be established first. requiresRegistry follows source references transitively and skips inputs replaced by a --source-location override.
7f21479 to
e1b1f32
Compare
Why
/v1/auth/validateeven when a signed license token already proves everything the validate call returns.Stacked on #2124 (commercial license election + generator token validation); base branch
build/bump-generator-license-election, retargetmainafter it merges.What Changed
Offline authentication
internal/license.Resolve), first configured source wins:SPEAKEASY_LICENSE_TOKEN(raw JWT) →SPEAKEASY_LICENSE_FILE(path to a JWT) →offline_license_tokenin~/.speakeasy/config.yaml. An unreadable, invalid, or other-workspace source is skipped with a warning naming the source (never the token) and authentication falls back to the platform.OfflineCapablefield onmodel.ExecutableCommand, set onspeakeasy runandspeakeasy generate sdk. Those commands authenticate viaauth.CommandContext, which prefers a usable offline license. All other authenticated commands authenticate online as before./v1/auth/validateis skipped. With a configured API key an SDK client is still installed, so registry uploads, telemetry, and workspace confirmation behave as online. Without an API key there is no SDK client; telemetry and registry are disabled at context construction and workspace confirmation is skipped.internal/sdkgen.evaluateGenerationAccess): an offline-license context is allowed without the platform access check — the signed token (expiry + target coverage, validated by the generator) is the access decision. A context with no SDK client is allowed iff it has a token. Otherwise the platform access check runs as before.Online re-authentication triggers
Each refreshes the stored license as a side effect:
auth.EnsureTargets— the offline license does not cover every selected target type (checked inWorkflow.Runpreparation and per target insdkgen).auth.EnsurePlatform— a platform API is needed but the offline context has no SDK client: GitHub workflow runs, andWorkflow.Runpreparation when the selected sources/targets hard-require the registry (frozen lockfile runs, registry inputs or overlays, blocking code-samples registry output). Best-effort registry interactions — source tracking, change reports, source publishing, non-blocking code samples — still skip when the registry is disabled, so typical migrated workflows keep running fully offline.Persistence
offline_license_token— only when one was issued, only non-free-tier, only for the authenticated workspace, and not inside GitHub Actions: a token persisted mid-job (e.g. bygenerate sdk versionbeforerun) would silently flip the same job's later commands to offline auth and bypass the per-generation platform access check, and the container is ephemeral anyway.speakeasy auth logoutclears it.speakeasy-selfis the default workspace remains a no-op (as onmain), preserving the stored customer id and token.speakeasy auth loginalways forces a fresh browser authentication, ignoring the configured API key — the explicit path to bypass the offline license and refresh the persisted one.Known limitations (offline, no API key)
speakeasy reprosuccess line is suppressed (events are never uploaded, so the ID would resolve to nothing).--watch) requires platform authentication.speakeasy auth loginrefreshes credentials.Testing
go build ./...; unit suite (excludingintegration) green.internal/auth/auth_test.go), token persistence rules incl. the speakeasy-self no-op (internal/config/config_test.go), license resolution (internal/license/license_test.go), registry-dependent workflow preparation incl. best-effort/non-blocking negatives (internal/run/prepare_test.go).$HOME: API key only / garbage token / stale token / other-workspace token / brokenSPEAKEASY_LICENSE_TOKEN→ online path, commercial output, fresh token stored. Valid token without API key → fully offline commercial generation. Valid token with API key → validate skipped (~3 s per command), token-based access.