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
40 changes: 22 additions & 18 deletions go/internal/sdkclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -318,28 +319,37 @@ type authKind int
const (
authNone authKind = iota
authBearer
authBasic
)

type authCredentials struct {
kind authKind
token string
userID string
apiKey string
kind authKind
token string
}

type credentialsFile struct {
UserID string `json:"user_id"`
APIKey string `json:"api_key"`
AuthToken string `json:"auth_token"`
}

// legacyAPIKeyMessage is returned for a legacy user (UUID) or org (sk-lit-) key. The SDK never sends Basic auth.
const legacyAPIKeyMessage = "LIGHTNING_API_KEY holds a legacy API key, which is no longer supported. Create a service-account key (lit_sa_) and set LIGHTNING_API_KEY to it. If you are on an older lightning-sdk, run: pip install -U lightning-sdk"

var legacyUUIDPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)

// apiKeyCredentials sends an API key as a Bearer token, refusing the legacy formats.
func apiKeyCredentials(apiKey string) (authCredentials, error) {
if strings.HasPrefix(apiKey, "sk-lit-") || legacyUUIDPattern.MatchString(apiKey) {
return authCredentials{}, errors.New(legacyAPIKeyMessage)
}
return authCredentials{kind: authBearer, token: apiKey}, nil
}

func resolveAuth() (authCredentials, error) {
if authToken := os.Getenv("LIGHTNING_AUTH_TOKEN"); authToken != "" {
return authCredentials{kind: authBearer, token: authToken}, nil
}
if userID, apiKey := os.Getenv("LIGHTNING_USER_ID"), os.Getenv("LIGHTNING_API_KEY"); userID != "" && apiKey != "" {
return authCredentials{kind: authBasic, userID: userID, apiKey: apiKey}, nil
if apiKey := os.Getenv("LIGHTNING_API_KEY"); apiKey != "" {
return apiKeyCredentials(apiKey)
}
credentials, ok, err := loadCredentialsFile()
if err != nil || !ok {
Expand All @@ -348,8 +358,8 @@ func resolveAuth() (authCredentials, error) {
if credentials.AuthToken != "" {
return authCredentials{kind: authBearer, token: credentials.AuthToken}, nil
}
if credentials.UserID != "" && credentials.APIKey != "" {
return authCredentials{kind: authBasic, userID: credentials.UserID, apiKey: credentials.APIKey}, nil
if credentials.APIKey != "" {
return apiKeyCredentials(credentials.APIKey)
}
return authCredentials{}, nil
}
Expand Down Expand Up @@ -386,20 +396,14 @@ func credentialsPath() (string, bool) {
}

func setGeneratedAuth(transport *httptransport.Runtime, auth authCredentials) {
switch auth.kind {
case authBearer:
if auth.kind == authBearer {
transport.DefaultAuthentication = httptransport.BearerToken(auth.token)
case authBasic:
transport.DefaultAuthentication = httptransport.BasicAuth(auth.userID, auth.apiKey)
}
}

func setRequestAuth(req *http.Request, auth authCredentials) {
switch auth.kind {
case authBearer:
if auth.kind == authBearer {
req.Header.Set("Authorization", "Bearer "+auth.token)
case authBasic:
req.SetBasicAuth(auth.userID, auth.apiKey)
}
}

Expand Down
47 changes: 21 additions & 26 deletions go/internal/sdkclient/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,18 +149,30 @@ func TestResolveAuthPrefersEnvBearer(t *testing.T) {
}
}

func TestResolveAuthUsesEnvBasic(t *testing.T) {
func TestResolveAuthUsesEnvAPIKeyAsBearer(t *testing.T) {
resetAuthEnv(t)
t.Setenv("LIGHTNING_USER_ID", "env-user")
t.Setenv("LIGHTNING_API_KEY", "env-key")
t.Setenv("LIGHTNING_API_KEY", "lit_sa_env-key")
writeCredentialsFile(t, `{"auth_token":"file-token"}`)

auth, err := resolveAuth()
if err != nil {
t.Fatalf("resolveAuth returned error: %v", err)
}
if auth.kind != authBasic || auth.userID != "env-user" || auth.apiKey != "env-key" {
t.Fatalf("auth = %+v, want env basic", auth)
if auth.kind != authBearer || auth.token != "lit_sa_env-key" {
t.Fatalf("auth = %+v, want env api key as bearer", auth)
}
}

func TestResolveAuthRefusesLegacyEnvAPIKey(t *testing.T) {
for _, key := range []string{"sk-lit-0123456789abcdef", "3f2b8c1e-9d4a-4b7f-8e21-0a6c5d9f7b10"} {
resetAuthEnv(t)
t.Setenv("LIGHTNING_API_KEY", key)

_, err := resolveAuth()
if err == nil || !strings.Contains(err.Error(), "no longer supported") {
t.Fatalf("resolveAuth(%q) error = %v, want migration message", key, err)
}
}
}

Expand All @@ -177,16 +189,13 @@ func TestResolveAuthLoadsCredentialsFileBearer(t *testing.T) {
}
}

func TestResolveAuthLoadsCredentialsFileBasic(t *testing.T) {
func TestResolveAuthRefusesLegacyCredentialsFileAPIKey(t *testing.T) {
resetAuthEnv(t)
writeCredentialsFile(t, `{"user_id":"file-user","api_key":"file-key"}`)
writeCredentialsFile(t, `{"user_id":"file-user","api_key":"sk-lit-0123456789abcdef"}`)

auth, err := resolveAuth()
if err != nil {
t.Fatalf("resolveAuth returned error: %v", err)
}
if auth.kind != authBasic || auth.userID != "file-user" || auth.apiKey != "file-key" {
t.Fatalf("auth = %+v, want file basic", auth)
_, err := resolveAuth()
if err == nil || !strings.Contains(err.Error(), "no longer supported") {
t.Fatalf("resolveAuth error = %v, want migration message", err)
}
}

Expand All @@ -199,20 +208,6 @@ func TestResolveAuthErrorsOnMalformedCredentialsFile(t *testing.T) {
}
}

func TestSetRequestAuthUsesResolvedBasicCredentials(t *testing.T) {
req := request(t, http.MethodGet, nil)

setRequestAuth(req, authCredentials{kind: authBasic, userID: "user-1", apiKey: "key-1"})

user, password, ok := req.BasicAuth()
if !ok {
t.Fatal("request is missing basic auth")
}
if user != "user-1" || password != "key-1" {
t.Fatalf("basic auth = %q %q, want user-1 key-1", user, password)
}
}

func TestSetRequestAuthUsesResolvedBearerCredentials(t *testing.T) {
req := request(t, http.MethodGet, nil)

Expand Down
26 changes: 21 additions & 5 deletions js/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,28 @@ export function resetSandboxConfig(): void {
globalConfig = {};
}

const LEGACY_API_KEY_MESSAGE =
"LIGHTNING_API_KEY holds a legacy API key, which is no longer supported. Create a service-account key (lit_sa_) and set LIGHTNING_API_KEY to it. If you are on an older @lightning-ai/sdk, upgrade it.";
const LEGACY_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

/** Reads LIGHTNING_API_KEY, refusing the legacy user (UUID) and org (sk-lit-) key formats. */
function legacyEnvApiKey(): string | undefined {
const key = process.env.LIGHTNING_API_KEY;
if (key && (key.startsWith("sk-lit-") || LEGACY_UUID_PATTERN.test(key))) {
throw new Error(LEGACY_API_KEY_MESSAGE);
}
return key;
}

function sandboxEnvApiKey(): string | undefined {
if (typeof process === "undefined") {
return undefined;
}
return process.env.LIGHTNING_SANDBOX_API_KEY ?? legacyEnvApiKey();
}

export function getApiKey(override?: string): string {
const env =
typeof process !== "undefined"
? (process.env.LIGHTNING_SANDBOX_API_KEY ?? process.env.LIGHTNING_API_KEY)
: undefined;
const key = override ?? globalConfig.apiKey ?? env;
const key = override ?? globalConfig.apiKey ?? sandboxEnvApiKey();
if (!key) {
throw new Error(
"Missing API key. Pass apiKey to Sandbox.configure(), provide it in create/get options, or set the LIGHTNING_SANDBOX_API_KEY environment variable.",
Expand Down
15 changes: 15 additions & 0 deletions js/tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ describe("config", () => {
assert.equal(getApiKey(), "k-env");
});

it("refuses a legacy UUID in LIGHTNING_API_KEY with the migration message", () => {
process.env.LIGHTNING_API_KEY = "3f2b8c1e-9d4a-4b7f-8e21-0a6c5d9f7b10";
assert.throws(() => getApiKey(), /no longer supported/);
});

it("refuses an sk-lit- key in LIGHTNING_API_KEY with the migration message", () => {
process.env.LIGHTNING_API_KEY = "sk-lit-0123456789abcdef";
assert.throws(() => getApiKey(), /no longer supported/);
});

it("does not read LIGHTNING_API_KEY when an explicit key is given", () => {
process.env.LIGHTNING_API_KEY = "sk-lit-0123456789abcdef";
assert.equal(getApiKey("override"), "override");
});

it("prefers LIGHTNING_SANDBOX_API_KEY over LIGHTNING_API_KEY", () => {
process.env.LIGHTNING_API_KEY = "k-legacy";
process.env.LIGHTNING_SANDBOX_API_KEY = "k-sandbox";
Expand Down
15 changes: 3 additions & 12 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,25 +106,20 @@ print(job.status)

```python
from lightning_sdk import Deployment, Machine
from lightning_sdk.api.deployment_api import ApiKeyAuth
from lightning_sdk.api.deployment_api import TokenAuth

deployment = Deployment("nginx-demo", teamspace="owner/teamspace")
deployment.start(
image="nginx:latest",
machine=Machine.CPU,
ports=80,
replicas=1,
auth=ApiKeyAuth(),
auth=TokenAuth(token="my-endpoint-token"),
)

print(deployment.status)
```

**Note:** `ApiKeyAuth` gates the endpoint on a personal Lightning API key. A
scoped API key is not one, so deploying with a scoped key leaves that key unable
to call the endpoint it just created. Use `TokenAuth` when the deploying key is
also the caller.

## Start sandboxes warm

A warm recipe says what the sandbox should already have done — packages
Expand Down Expand Up @@ -188,13 +183,9 @@ lightning deployment create nginx-demo \
--machine CPU \
--port 80 \
--replicas 1 \
--api-key-auth
--token-auth my-endpoint-token
```

**Note:** `--api-key-auth` gates the endpoint on a personal Lightning API key.
Deploying from automation with a scoped API key leaves that key unable to call
the endpoint. Use `--token-auth` when the deploying key is also the caller.

Inspect and stream logs:

```bash
Expand Down
8 changes: 0 additions & 8 deletions python/docs/cli/api-key.rst

This file was deleted.

1 change: 0 additions & 1 deletion python/docs/cli/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
("lightning container delete", "name"): "Container name to delete.",
("lightning model upload", "name"): "Model name to upload.",
("lightning model download", "name"): "Model name to download.",
("lightning api-key delete", "key_id"): "API key ID to delete.",
("lightning file upload", "path"): "Local file path to upload.",
("lightning file download", "path"): "Remote file path to download.",
("lightning folder upload", "path"): "Local folder path to upload.",
Expand Down
3 changes: 1 addition & 2 deletions python/docs/cli/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Common Workflows
* Build and operate inference services with :doc:`deployment` and :doc:`model`.
* Move data and artifacts with :doc:`file`, :doc:`folder`, :doc:`container`, and :doc:`cp`.
* Configure accounts, organizations, teamspaces, cloud accounts, and SSH with
:doc:`config`, :doc:`api-key`, and :doc:`ssh`.
:doc:`config`, and :doc:`ssh`.
* Manage lower-level sandbox sessions with :doc:`sandbox`.
* Point coding agents (OpenCode, pi, Codex, DeepSeek Harness, Cursor) at code.lightning.ai with :doc:`code`.

Expand All @@ -116,7 +116,6 @@ reference.
deployment
container
model
api-key
file
folder
ssh
Expand Down
3 changes: 0 additions & 3 deletions python/docs/sdk/api/deployment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ Deployment
.. autoclass:: TokenAuth
:members:

.. autoclass:: ApiKeyAuth
:members:

.. autoclass:: ReleaseStrategy
:members:

Expand Down
Loading
Loading