@raster-app/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +45 -46
  2. package/dist/index.js +866 -80
  3. package/package.json +2 -6
package/dist/index.js CHANGED
@@ -16337,14 +16337,11 @@ config(en_default());
16337
16337
  // package.json
16338
16338
  var package_default = {
16339
16339
  name: "@raster-app/cli",
16340
- version: "0.2.0",
16340
+ version: "0.3.0",
16341
16341
  description: "Command-line client for the Raster API",
16342
16342
  license: "Apache-2.0",
16343
16343
  type: "module",
16344
- repository: {
16345
- type: "git",
16346
- url: "https://github.com/raster-app/raster-cli"
16347
- },
16344
+ homepage: "https://docs.raster.app/api/cli",
16348
16345
  engines: {
16349
16346
  node: ">=20"
16350
16347
  },
@@ -16357,7 +16354,6 @@ var package_default = {
16357
16354
  ],
16358
16355
  scripts: {
16359
16356
  build: "bun build src/index.ts --outdir dist --target node --banner '#!/usr/bin/env node'",
16360
- "build:binary": "bun build src/index.ts --compile --target=bun-darwin-arm64 --outfile raster",
16361
16357
  "gen:types": "bun scripts/gen-types.ts",
16362
16358
  "check:types": "tsc --noEmit",
16363
16359
  "check:tarball": "bun scripts/check-tarball.ts",
@@ -16891,18 +16887,27 @@ class ApiError extends CliError {
16891
16887
  // src/api/client.ts
16892
16888
  var VERSION_HEADER = { "Api-Version": API_VERSION };
16893
16889
  var LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
16894
- function validateBaseUrl(rawUrl) {
16890
+ function isRasterHost(hostname3) {
16891
+ return hostname3 === "raster.app" || hostname3.endsWith(".raster.app");
16892
+ }
16893
+ function assertRasterOrLocalHost(rawUrl, label) {
16895
16894
  let parsed;
16896
16895
  try {
16897
16896
  parsed = new URL(rawUrl);
16898
16897
  } catch {
16899
- throw new UsageError(`Invalid API base URL: ${rawUrl}`);
16898
+ throw new UsageError(`Invalid ${label}: ${rawUrl}`);
16899
+ }
16900
+ if (LOCAL_HOSTNAMES.has(parsed.hostname)) {
16901
+ return rawUrl.replace(/\/+$/, "");
16900
16902
  }
16901
- if (parsed.protocol !== "https:" && !LOCAL_HOSTNAMES.has(parsed.hostname)) {
16902
- throw new UsageError(`API base URL must use https (got ${rawUrl}). localhost and 127.0.0.1 are the only http exceptions.`);
16903
+ if (parsed.protocol !== "https:" || !isRasterHost(parsed.hostname)) {
16904
+ throw new UsageError(`${label} must be https on a raster.app host (got ${rawUrl}); localhost is allowed for development.`);
16903
16905
  }
16904
16906
  return rawUrl.replace(/\/+$/, "");
16905
16907
  }
16908
+ function validateBaseUrl(rawUrl) {
16909
+ return assertRasterOrLocalHost(rawUrl, "API base URL");
16910
+ }
16906
16911
  function buildRequestHeaders(apiKey) {
16907
16912
  const headers = { "Api-Version": API_VERSION };
16908
16913
  if (apiKey)
@@ -17031,10 +17036,23 @@ function formatBytes(size) {
17031
17036
  var CONFIG_FILE_NAME = "config.json";
17032
17037
  var CONFIG_FILE_MODE = 384;
17033
17038
  var CONFIG_DIR_MODE = 448;
17039
+ var oauthTokensSchema = exports_external.object({
17040
+ accessToken: exports_external.string().min(1),
17041
+ refreshToken: exports_external.string().min(1),
17042
+ expiresAt: exports_external.number(),
17043
+ issuer: exports_external.url(),
17044
+ scope: exports_external.string(),
17045
+ resource: exports_external.url()
17046
+ });
17047
+ var oauthClientSchema = exports_external.object({ clientId: exports_external.string().min(1) });
17034
17048
  var configSchema = exports_external.object({
17035
17049
  apiKey: exports_external.string().min(1).optional(),
17036
17050
  organizationId: exports_external.string().min(1).optional(),
17037
- libraries: exports_external.array(exports_external.string()).optional()
17051
+ libraries: exports_external.array(exports_external.string()).optional(),
17052
+ oauth: oauthTokensSchema.optional(),
17053
+ clients: exports_external.record(exports_external.string(), oauthClientSchema).optional(),
17054
+ defaultOrg: exports_external.string().min(1).optional(),
17055
+ issuer: exports_external.url().optional()
17038
17056
  });
17039
17057
  function configDir(env) {
17040
17058
  if (env.RASTER_CONFIG_HOME)
@@ -17070,13 +17088,334 @@ async function writeConfig(config2, env) {
17070
17088
  `, { mode: CONFIG_FILE_MODE });
17071
17089
  await chmod(path2, CONFIG_FILE_MODE);
17072
17090
  }
17073
- function resolveApiKey(options) {
17091
+ async function writeOAuthTokens(tokens, env) {
17092
+ const config2 = await readConfig(env);
17093
+ await writeConfig({ ...config2, oauth: tokens }, env);
17094
+ }
17095
+ async function clearOAuthTokens(env) {
17096
+ const config2 = await readConfig(env);
17097
+ delete config2.oauth;
17098
+ await writeConfig(config2, env);
17099
+ }
17100
+ async function getCachedClientId(issuer, env) {
17101
+ return (await readConfig(env)).clients?.[issuer]?.clientId ?? null;
17102
+ }
17103
+ async function cacheClientId(issuer, clientId, env) {
17104
+ const config2 = await readConfig(env);
17105
+ await writeConfig({ ...config2, clients: { ...config2.clients, [issuer]: { clientId } } }, env);
17106
+ }
17107
+
17108
+ // src/oauth/issuer.ts
17109
+ var DEFAULT_ISSUER = "https://raster.app";
17110
+ var RESOURCE_INDICATOR = "https://api.raster.app";
17111
+ var USER_SCOPE = "user";
17112
+ var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
17113
+ function resolveIssuer(options) {
17114
+ const raw = options.env.RASTER_AUTH_ISSUER ?? options.config.issuer ?? DEFAULT_ISSUER;
17115
+ return assertRasterOrLocalHost(raw, "auth issuer");
17116
+ }
17117
+
17118
+ // src/oauth/open-browser.ts
17119
+ import { spawn } from "node:child_process";
17120
+ import { platform } from "node:os";
17121
+ function openerCommand() {
17122
+ const current = platform();
17123
+ if (current === "darwin")
17124
+ return { command: "open", args: [] };
17125
+ if (current === "win32")
17126
+ return { command: "cmd", args: ["/c", "start", ""] };
17127
+ if (current === "linux")
17128
+ return { command: "xdg-open", args: [] };
17129
+ return null;
17130
+ }
17131
+ function canOpenBrowser() {
17132
+ if (!openerCommand())
17133
+ return false;
17134
+ if (platform() === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
17135
+ return false;
17136
+ }
17137
+ return true;
17138
+ }
17139
+ async function openBrowser(url2) {
17140
+ const opener = openerCommand();
17141
+ if (!opener)
17142
+ throw new Error("No browser opener is available on this platform.");
17143
+ await new Promise((resolve, reject) => {
17144
+ const child = spawn(opener.command, [...opener.args, url2], { stdio: "ignore", detached: true });
17145
+ child.once("error", reject);
17146
+ child.once("spawn", () => {
17147
+ child.unref();
17148
+ resolve();
17149
+ });
17150
+ });
17151
+ }
17152
+
17153
+ // src/oauth/http.ts
17154
+ var FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
17155
+ var OAUTH_REQUEST_TIMEOUT_MS = 30000;
17156
+ var oauthErrorSchema = exports_external.object({
17157
+ error: exports_external.string(),
17158
+ error_description: exports_external.string().optional()
17159
+ });
17160
+
17161
+ class OAuthError extends CliError {
17162
+ error;
17163
+ constructor(error51, description, status) {
17164
+ super(description ? `${error51}: ${description}` : error51, EXIT_CODES.auth);
17165
+ this.error = error51;
17166
+ this.status = status;
17167
+ }
17168
+ status;
17169
+ }
17170
+
17171
+ class AuthServerHttpError extends CliError {
17172
+ status;
17173
+ constructor(status) {
17174
+ super(`Authorization server request failed with HTTP ${status}.`, EXIT_CODES.auth);
17175
+ this.status = status;
17176
+ }
17177
+ }
17178
+ function parseOAuthError(body, status) {
17179
+ const parsed = oauthErrorSchema.safeParse(body);
17180
+ if (!parsed.success)
17181
+ return null;
17182
+ return new OAuthError(parsed.data.error, parsed.data.error_description, status);
17183
+ }
17184
+ async function readJson(response) {
17185
+ try {
17186
+ return await response.json();
17187
+ } catch {
17188
+ return null;
17189
+ }
17190
+ }
17191
+ async function postForm(url2, form, fetch) {
17192
+ let response;
17193
+ try {
17194
+ response = await fetch(url2, {
17195
+ method: "POST",
17196
+ headers: { "Content-Type": FORM_CONTENT_TYPE, Accept: "application/json" },
17197
+ body: new URLSearchParams(form).toString(),
17198
+ signal: AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS)
17199
+ });
17200
+ } catch (error51) {
17201
+ throw new NetworkError(`Could not reach the authorization server: ${describeError(error51)}`);
17202
+ }
17203
+ const body = await readJson(response);
17204
+ const oauthError = parseOAuthError(body, response.status);
17205
+ if (oauthError)
17206
+ throw oauthError;
17207
+ if (!response.ok) {
17208
+ throw new AuthServerHttpError(response.status);
17209
+ }
17210
+ return body;
17211
+ }
17212
+ async function postJson(url2, payload, fetch) {
17213
+ let response;
17214
+ try {
17215
+ response = await fetch(url2, {
17216
+ method: "POST",
17217
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
17218
+ body: JSON.stringify(payload),
17219
+ signal: AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS)
17220
+ });
17221
+ } catch (error51) {
17222
+ throw new NetworkError(`Could not reach the authorization server: ${describeError(error51)}`);
17223
+ }
17224
+ const body = await readJson(response);
17225
+ const oauthError = parseOAuthError(body, response.status);
17226
+ if (oauthError)
17227
+ throw oauthError;
17228
+ if (!response.ok) {
17229
+ throw new AuthServerHttpError(response.status);
17230
+ }
17231
+ return body;
17232
+ }
17233
+ async function getJson(url2, fetch) {
17234
+ let response;
17235
+ try {
17236
+ response = await fetch(url2, {
17237
+ method: "GET",
17238
+ headers: { Accept: "application/json" },
17239
+ signal: AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS)
17240
+ });
17241
+ } catch (error51) {
17242
+ throw new NetworkError(`Could not reach the authorization server: ${describeError(error51)}`);
17243
+ }
17244
+ if (!response.ok) {
17245
+ throw new AuthServerHttpError(response.status);
17246
+ }
17247
+ return readJson(response);
17248
+ }
17249
+ function describeError(error51) {
17250
+ return error51 instanceof Error ? error51.message : String(error51);
17251
+ }
17252
+
17253
+ // src/oauth/discovery.ts
17254
+ var WELL_KNOWN_PATH = "/.well-known/oauth-authorization-server";
17255
+ var metadataSchema = exports_external.object({
17256
+ issuer: exports_external.string(),
17257
+ authorization_endpoint: exports_external.url(),
17258
+ token_endpoint: exports_external.url(),
17259
+ device_authorization_endpoint: exports_external.url(),
17260
+ registration_endpoint: exports_external.url(),
17261
+ revocation_endpoint: exports_external.url()
17262
+ });
17263
+ var metadataCache = new Map;
17264
+ async function discover(issuer, fetch) {
17265
+ const cached2 = metadataCache.get(issuer);
17266
+ if (cached2)
17267
+ return cached2;
17268
+ const body = await getJson(`${issuer}${WELL_KNOWN_PATH}`, fetch);
17269
+ const parsed = metadataSchema.safeParse(body);
17270
+ if (!parsed.success) {
17271
+ throw new CliError(`The authorization server metadata at ${issuer} is missing required endpoints.`, EXIT_CODES.auth);
17272
+ }
17273
+ const metadata = {
17274
+ issuer: parsed.data.issuer,
17275
+ authorizationEndpoint: assertRasterOrLocalHost(parsed.data.authorization_endpoint, "authorization_endpoint"),
17276
+ tokenEndpoint: assertRasterOrLocalHost(parsed.data.token_endpoint, "token_endpoint"),
17277
+ deviceAuthorizationEndpoint: assertRasterOrLocalHost(parsed.data.device_authorization_endpoint, "device_authorization_endpoint"),
17278
+ registrationEndpoint: assertRasterOrLocalHost(parsed.data.registration_endpoint, "registration_endpoint"),
17279
+ revocationEndpoint: assertRasterOrLocalHost(parsed.data.revocation_endpoint, "revocation_endpoint")
17280
+ };
17281
+ metadataCache.set(issuer, metadata);
17282
+ return metadata;
17283
+ }
17284
+
17285
+ // src/oauth/register.ts
17286
+ var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
17287
+ var REGISTRATION_REQUEST = {
17288
+ client_name: "Raster CLI",
17289
+ token_endpoint_auth_method: "none",
17290
+ grant_types: [DEVICE_CODE_GRANT, "authorization_code", "refresh_token"],
17291
+ response_types: ["code"],
17292
+ redirect_uris: ["http://127.0.0.1/callback"]
17293
+ };
17294
+ var registrationResponseSchema = exports_external.object({ client_id: exports_external.string().min(1) });
17295
+ async function ensureClientId(options) {
17296
+ if (options.pinnedClientId)
17297
+ return options.pinnedClientId;
17298
+ const cached2 = await getCachedClientId(options.issuer, options.env);
17299
+ if (cached2)
17300
+ return cached2;
17301
+ const body = await postJson(options.metadata.registrationEndpoint, REGISTRATION_REQUEST, options.fetch);
17302
+ const parsed = registrationResponseSchema.safeParse(body);
17303
+ if (!parsed.success) {
17304
+ throw new CliError("Dynamic client registration did not return a client_id.", EXIT_CODES.auth);
17305
+ }
17306
+ await cacheClientId(options.issuer, parsed.data.client_id, options.env);
17307
+ return parsed.data.client_id;
17308
+ }
17309
+
17310
+ // src/oauth/tokens.ts
17311
+ var ACCESS_TOKEN_SKEW_MS = 60000;
17312
+ var SECONDS_TO_MS = 1000;
17313
+ var tokenResponseSchema = exports_external.object({
17314
+ access_token: exports_external.string().min(1),
17315
+ refresh_token: exports_external.string().min(1),
17316
+ expires_in: exports_external.number().positive(),
17317
+ token_type: exports_external.string().optional(),
17318
+ scope: exports_external.string().optional()
17319
+ });
17320
+ function parseTokenResponse(body) {
17321
+ const parsed = tokenResponseSchema.parse(body);
17322
+ return {
17323
+ accessToken: parsed.access_token,
17324
+ refreshToken: parsed.refresh_token,
17325
+ expiresIn: parsed.expires_in,
17326
+ scope: parsed.scope
17327
+ };
17328
+ }
17329
+ function toStoredTokens(response, context) {
17330
+ return {
17331
+ accessToken: response.accessToken,
17332
+ refreshToken: response.refreshToken,
17333
+ expiresAt: context.now + response.expiresIn * SECONDS_TO_MS,
17334
+ issuer: context.issuer,
17335
+ scope: response.scope ?? context.fallbackScope ?? "user",
17336
+ resource: context.resource
17337
+ };
17338
+ }
17339
+ function isAccessTokenFresh(tokens, now, skewMs = ACCESS_TOKEN_SKEW_MS) {
17340
+ return now < tokens.expiresAt - skewMs;
17341
+ }
17342
+
17343
+ // src/oauth/refresh.ts
17344
+ var INVALID_GRANT = "invalid_grant";
17345
+
17346
+ class OAuthReuseError extends CliError {
17347
+ constructor() {
17348
+ super("Your saved login is no longer valid. Run `raster auth login` to sign in again.", EXIT_CODES.auth);
17349
+ }
17350
+ }
17351
+ async function refreshTokens(options) {
17352
+ let body;
17353
+ try {
17354
+ body = await postForm(options.metadata.tokenEndpoint, {
17355
+ grant_type: "refresh_token",
17356
+ refresh_token: options.tokens.refreshToken,
17357
+ client_id: options.clientId,
17358
+ resource: RESOURCE_INDICATOR
17359
+ }, options.fetch);
17360
+ } catch (error51) {
17361
+ if (error51 instanceof OAuthError && error51.error === INVALID_GRANT)
17362
+ throw new OAuthReuseError;
17363
+ if (error51 instanceof AuthServerHttpError && error51.status >= 400 && error51.status < 500) {
17364
+ throw new OAuthReuseError;
17365
+ }
17366
+ throw error51;
17367
+ }
17368
+ return toStoredTokens(parseTokenResponse(body), {
17369
+ issuer: options.tokens.issuer,
17370
+ resource: options.tokens.resource,
17371
+ now: options.now,
17372
+ fallbackScope: options.tokens.scope
17373
+ });
17374
+ }
17375
+
17376
+ // src/oauth/session.ts
17377
+ async function ensureFreshOAuthSession(options) {
17378
+ if (isAccessTokenFresh(options.tokens, options.now)) {
17379
+ return { status: "active", tokens: options.tokens };
17380
+ }
17381
+ const issuer = assertRasterOrLocalHost(options.tokens.issuer, "auth issuer");
17382
+ const metadata = await discover(issuer, options.fetch);
17383
+ const clientId = await ensureClientId({
17384
+ issuer,
17385
+ metadata,
17386
+ env: options.env,
17387
+ fetch: options.fetch,
17388
+ pinnedClientId: options.pinnedClientId
17389
+ });
17390
+ try {
17391
+ const rotated = await refreshTokens({
17392
+ tokens: options.tokens,
17393
+ metadata,
17394
+ clientId,
17395
+ fetch: options.fetch,
17396
+ now: options.now
17397
+ });
17398
+ await writeOAuthTokens(rotated, options.env);
17399
+ return { status: "active", tokens: rotated };
17400
+ } catch (error51) {
17401
+ if (error51 instanceof OAuthReuseError) {
17402
+ await clearOAuthTokens(options.env);
17403
+ return { status: "reauth-required" };
17404
+ }
17405
+ throw error51;
17406
+ }
17407
+ }
17408
+
17409
+ // src/lib/credential.ts
17410
+ function resolveCredential(options) {
17074
17411
  if (options.flagKey)
17075
- return { key: options.flagKey, source: "flag" };
17412
+ return { kind: "apiKey", key: options.flagKey, source: "flag" };
17413
+ if (options.config.oauth)
17414
+ return { kind: "oauth", tokens: options.config.oauth };
17076
17415
  if (options.env.RASTER_API_KEY)
17077
- return { key: options.env.RASTER_API_KEY, source: "env" };
17416
+ return { kind: "apiKey", key: options.env.RASTER_API_KEY, source: "env" };
17078
17417
  if (options.config.apiKey)
17079
- return { key: options.config.apiKey, source: "config" };
17418
+ return { kind: "apiKey", key: options.config.apiKey, source: "config" };
17080
17419
  return null;
17081
17420
  }
17082
17421
 
@@ -17193,10 +17532,15 @@ function promptSecret(message) {
17193
17532
  }
17194
17533
 
17195
17534
  // src/lib/context.ts
17535
+ var RASTER_ORG_ENV = "RASTER_ORG";
17536
+ var PINNED_CLIENT_ID_ENV = "RASTER_OAUTH_CLIENT_ID";
17537
+ function toLibraryIds(libraries) {
17538
+ return (libraries ?? []).map((entry) => typeof entry === "string" ? entry : entry?.library).filter((id) => typeof id === "string");
17539
+ }
17196
17540
  async function createCommandContext(globals, overrides = {}) {
17197
17541
  const env = overrides.env ?? process.env;
17198
17542
  const config2 = await readConfig(env);
17199
- const apiKey = resolveApiKey({ flagKey: globals.apiKey, env, config: config2 });
17543
+ const now = overrides.now ?? Date.now;
17200
17544
  const output = {
17201
17545
  json: globals.json === true,
17202
17546
  verbose: globals.verbose === true,
@@ -17205,61 +17549,126 @@ async function createCommandContext(globals, overrides = {}) {
17205
17549
  };
17206
17550
  const baseUrl = validateBaseUrl(env.RASTER_API_BASE_URL ?? DEFAULT_BASE_URL);
17207
17551
  const baseFetch = overrides.fetch ?? globalThis.fetch;
17208
- const instrumentedFetch = instrumentFetch(baseFetch, output, apiKey ? maskApiKey(apiKey.key) : null);
17552
+ const credential = await resolveActiveCredential({
17553
+ flagKey: globals.apiKey,
17554
+ env,
17555
+ config: config2,
17556
+ fetch: instrumentFetch(baseFetch, output, null),
17557
+ now: now()
17558
+ });
17559
+ const apiKey = credential?.kind === "apiKey" ? { key: credential.key, source: credential.source } : null;
17560
+ const bearer = credentialBearer(credential);
17561
+ const instrumentedFetch = instrumentFetch(baseFetch, output, bearer ? maskApiKey(bearer) : null);
17209
17562
  const clientFactory = (clientOverrides) => createApiClient({
17210
17563
  baseUrl,
17211
- apiKey: clientOverrides ? clientOverrides.apiKey : apiKey?.key ?? null,
17564
+ apiKey: clientOverrides ? clientOverrides.apiKey : bearer,
17212
17565
  fetch: instrumentedFetch
17213
17566
  });
17214
17567
  let viewerPromise = null;
17215
17568
  const loadViewer = () => {
17216
17569
  if (!viewerPromise) {
17217
- if (!apiKey) {
17218
- throw new AuthSetupError("No API key found. Run `raster auth login` or set RASTER_API_KEY.");
17570
+ if (!credential) {
17571
+ throw new AuthSetupError("Not authenticated. Run `raster auth login` or set RASTER_API_KEY.");
17219
17572
  }
17220
- viewerPromise = unwrapResult(() => clientFactory({ apiKey: apiKey.key }).GET("/me", { params: { header: VERSION_HEADER } }));
17573
+ viewerPromise = unwrapResult(() => clientFactory().GET("/me", { params: { header: VERSION_HEADER } }));
17221
17574
  }
17222
17575
  return viewerPromise;
17223
17576
  };
17224
17577
  const cacheMatchesKey = config2.apiKey !== undefined && apiKey?.key === config2.apiKey;
17578
+ const resolveOrgId = async () => {
17579
+ if (globals.org)
17580
+ return globals.org;
17581
+ if (env[RASTER_ORG_ENV])
17582
+ return env[RASTER_ORG_ENV];
17583
+ if (credential?.kind === "oauth") {
17584
+ if (credential.tokens.scope === "org") {
17585
+ const viewer2 = await loadViewer();
17586
+ if (viewer2.organizationId)
17587
+ return viewer2.organizationId;
17588
+ throw new CliError("This login is bound to an organization that is no longer available. Run `raster auth login` again.", EXIT_CODES.auth);
17589
+ }
17590
+ if (config2.defaultOrg)
17591
+ return config2.defaultOrg;
17592
+ throw new UsageError("This login can access multiple organizations. Pass --org <id> (or set RASTER_ORG); run `raster whoami` to list them.");
17593
+ }
17594
+ if (cacheMatchesKey && config2.organizationId)
17595
+ return config2.organizationId;
17596
+ const viewer = await loadViewer();
17597
+ if (!viewer.organizationId) {
17598
+ throw new CliError("Could not determine the organization from the API key.", EXIT_CODES.generic);
17599
+ }
17600
+ return viewer.organizationId;
17601
+ };
17602
+ const librariesForTarget = async () => {
17603
+ const cached2 = cacheMatchesKey ? config2.libraries : undefined;
17604
+ if (cached2)
17605
+ return cached2;
17606
+ const viewer = await loadViewer();
17607
+ if (credential?.kind === "oauth" && credential.tokens.scope !== "org") {
17608
+ const organizationId = await resolveOrgId();
17609
+ const entry = viewer.organizations?.find((organization) => organization?.organizationId === organizationId);
17610
+ return entry?.libraries ?? [];
17611
+ }
17612
+ return viewer.libraries ?? [];
17613
+ };
17225
17614
  return {
17226
17615
  ...output,
17227
17616
  env,
17228
17617
  baseUrl,
17229
17618
  isInteractive: overrides.isInteractive ?? process.stdin.isTTY === true,
17619
+ credential,
17230
17620
  apiKey,
17621
+ bearerToken: bearer,
17231
17622
  fetch: instrumentedFetch,
17232
17623
  clientFactory,
17233
17624
  confirm: overrides.confirm ?? promptConfirm,
17234
17625
  promptSecret: overrides.promptSecret ?? promptSecret,
17626
+ now,
17627
+ issuer: resolveIssuer({ env, config: config2 }),
17628
+ pinnedClientId: env[PINNED_CLIENT_ID_ENV],
17629
+ openBrowser: overrides.openBrowser ?? openBrowser,
17630
+ canOpenBrowser: overrides.canOpenBrowser ?? canOpenBrowser,
17235
17631
  explicitLibrary: globals.library,
17236
- resolveOrg: async () => {
17237
- if (globals.org)
17238
- return globals.org;
17239
- if (cacheMatchesKey && config2.organizationId)
17240
- return config2.organizationId;
17241
- const viewer = await loadViewer();
17242
- if (!viewer.organizationId) {
17243
- throw new CliError("Could not determine the organization from the API key.", EXIT_CODES.generic);
17244
- }
17245
- return viewer.organizationId;
17246
- },
17632
+ resolveOrg: resolveOrgId,
17247
17633
  resolveLibrary: async () => {
17248
17634
  if (globals.library)
17249
17635
  return globals.library;
17250
- const cachedLibraries = cacheMatchesKey ? config2.libraries : undefined;
17251
- const source = cachedLibraries ?? (await loadViewer()).libraries ?? [];
17252
- const libraries = source.filter((value) => typeof value === "string");
17636
+ const libraries = toLibraryIds(await librariesForTarget());
17253
17637
  const onlyLibrary = libraries[0];
17254
17638
  if (libraries.length === 1 && onlyLibrary)
17255
17639
  return onlyLibrary;
17256
17640
  if (libraries.length === 0) {
17257
- throw new AuthSetupError("The API key has no library access.");
17641
+ throw new AuthSetupError("No library access for this login. Pass --library <id>.");
17258
17642
  }
17259
- throw new UsageError(`The API key can access ${libraries.length} libraries (${libraries.join(", ")}). Pass --library <id>.`);
17643
+ throw new UsageError(`This login can access ${libraries.length} libraries (${libraries.join(", ")}). Pass --library <id>.`);
17260
17644
  }
17261
17645
  };
17262
17646
  }
17647
+ function credentialBearer(credential) {
17648
+ if (!credential)
17649
+ return null;
17650
+ return credential.kind === "apiKey" ? credential.key : credential.tokens.accessToken;
17651
+ }
17652
+ async function resolveActiveCredential(options) {
17653
+ const credential = resolveCredential({ flagKey: options.flagKey, env: options.env, config: options.config });
17654
+ if (credential?.kind !== "oauth")
17655
+ return credential;
17656
+ const session = await ensureFreshOAuthSession({
17657
+ tokens: credential.tokens,
17658
+ env: options.env,
17659
+ fetch: options.fetch,
17660
+ pinnedClientId: options.env[PINNED_CLIENT_ID_ENV],
17661
+ now: options.now
17662
+ });
17663
+ if (session.status === "reauth-required") {
17664
+ return resolveCredential({
17665
+ flagKey: options.flagKey,
17666
+ env: options.env,
17667
+ config: { ...options.config, oauth: undefined }
17668
+ });
17669
+ }
17670
+ return { kind: "oauth", tokens: session.tokens };
17671
+ }
17263
17672
  function instrumentFetch(baseFetch, output, maskedKey) {
17264
17673
  return async (input, init) => {
17265
17674
  const request = input instanceof Request ? input : new Request(String(input), init);
@@ -17276,10 +17685,15 @@ function instrumentFetch(baseFetch, output, maskedKey) {
17276
17685
  }
17277
17686
  };
17278
17687
  }
17279
- function requireApiKey(context) {
17280
- if (context.apiKey)
17281
- return context.apiKey;
17282
- throw new AuthSetupError("No API key found. Run `raster auth login` or set RASTER_API_KEY.");
17688
+ function requireAuth(context) {
17689
+ if (context.credential)
17690
+ return context.credential;
17691
+ throw new AuthSetupError("Not authenticated. Run `raster auth login` or set RASTER_API_KEY.");
17692
+ }
17693
+ function requireBearer(context) {
17694
+ if (context.bearerToken)
17695
+ return context.bearerToken;
17696
+ throw new AuthSetupError("Not authenticated. Run `raster auth login` or set RASTER_API_KEY.");
17283
17697
  }
17284
17698
  async function executeCommand(context, action) {
17285
17699
  try {
@@ -17333,7 +17747,7 @@ async function listAssetsCommand(context, options) {
17333
17747
  if (options.tag.length > MAX_TAGS_PER_QUERY) {
17334
17748
  throw new ValidationError(`At most ${MAX_TAGS_PER_QUERY} --tag filters are supported per query.`);
17335
17749
  }
17336
- requireApiKey(context);
17750
+ requireAuth(context);
17337
17751
  const organizationId = await context.resolveOrg();
17338
17752
  const libraryId = await context.resolveLibrary();
17339
17753
  const client = context.clientFactory();
@@ -17355,7 +17769,7 @@ async function listAssetsCommand(context, options) {
17355
17769
  renderTable(context, assets, ASSET_COLUMNS);
17356
17770
  }
17357
17771
  async function getAssetCommand(context, assetId) {
17358
- requireApiKey(context);
17772
+ requireAuth(context);
17359
17773
  const asset = await fetchAsset(context, assetId);
17360
17774
  if (context.json) {
17361
17775
  printJson(context, asset);
@@ -17385,7 +17799,7 @@ var searchOptionsSchema = exports_external.object({
17385
17799
  pageSize: exports_external.coerce.number().int().min(1).optional()
17386
17800
  });
17387
17801
  async function searchAssetsCommand(context, query, options) {
17388
- requireApiKey(context);
17802
+ requireAuth(context);
17389
17803
  const organizationId = await context.resolveOrg();
17390
17804
  const client = context.clientFactory();
17391
17805
  const result = await unwrapResult(() => client.GET("/organizations/{organizationId}/search/assets", {
@@ -17412,7 +17826,7 @@ var downloadOptionsSchema = exports_external.object({
17412
17826
  force: exports_external.boolean().default(false)
17413
17827
  });
17414
17828
  async function downloadAssetCommand(context, assetId, options) {
17415
- requireApiKey(context);
17829
+ requireAuth(context);
17416
17830
  const asset = await fetchAsset(context, assetId);
17417
17831
  const fileUrl = asset.url;
17418
17832
  if (!fileUrl)
@@ -17440,7 +17854,7 @@ var uploadedAssetsSchema = exports_external.object({
17440
17854
  async function uploadAssetsCommand(context, files) {
17441
17855
  if (files.length === 0)
17442
17856
  throw new UsageError("Provide at least one file to upload.");
17443
- const { key } = requireApiKey(context);
17857
+ const bearer = requireBearer(context);
17444
17858
  const organizationId = await context.resolveOrg();
17445
17859
  const libraryId = await context.resolveLibrary();
17446
17860
  const batches = chunk(files, MAX_UPLOAD_FILES);
@@ -17460,7 +17874,7 @@ async function uploadAssetsCommand(context, files) {
17460
17874
  if (batches.length > 1) {
17461
17875
  note(context, `Uploading batch ${batchIndex + 1}/${batches.length} (${batch.length} files)…`);
17462
17876
  }
17463
- const response = await context.fetch(`${context.baseUrl}/organizations/${organizationId}/libraries/${libraryId}/assets`, { method: "POST", headers: buildRequestHeaders(key), body: formData });
17877
+ const response = await context.fetch(`${context.baseUrl}/organizations/${organizationId}/libraries/${libraryId}/assets`, { method: "POST", headers: buildRequestHeaders(bearer), body: formData });
17464
17878
  const payload = await parseEnvelope(response);
17465
17879
  payloads.push(payload);
17466
17880
  const parsed = uploadedAssetsSchema.safeParse(payload);
@@ -17487,7 +17901,7 @@ async function removeAssetsCommand(context, assetIds, options) {
17487
17901
  if (assetIds.length > MAX_DELETE_IDS) {
17488
17902
  throw new ValidationError(`At most ${MAX_DELETE_IDS} asset ids are supported per call.`);
17489
17903
  }
17490
- requireApiKey(context);
17904
+ requireAuth(context);
17491
17905
  const organizationId = await context.resolveOrg();
17492
17906
  const libraryId = await context.resolveLibrary();
17493
17907
  if (!options.yes && context.isInteractive) {
@@ -17512,7 +17926,7 @@ var describeOptionsSchema = exports_external.object({
17512
17926
  text: exports_external.string().min(1)
17513
17927
  });
17514
17928
  async function describeAssetCommand(context, assetId, options) {
17515
- requireApiKey(context);
17929
+ requireAuth(context);
17516
17930
  const organizationId = await context.resolveOrg();
17517
17931
  const libraryId = await context.resolveLibrary();
17518
17932
  const client = context.clientFactory();
@@ -17535,7 +17949,7 @@ var transferOptionsSchema = exports_external.object({
17535
17949
  async function transferAssetsCommand(context, assetIds, options) {
17536
17950
  if (assetIds.length === 0)
17537
17951
  throw new UsageError("Provide at least one asset id.");
17538
- requireApiKey(context);
17952
+ requireAuth(context);
17539
17953
  const organizationId = await context.resolveOrg();
17540
17954
  const libraryId = await context.resolveLibrary();
17541
17955
  const client = context.clientFactory();
@@ -17561,24 +17975,329 @@ function registerAssetCommands(program2, runAction) {
17561
17975
  assets.command("transfer").description("Move assets to another library").argument("<assetIds...>", "Asset ids to move").requiredOption("--to <libraryId>", "Target library id").action((assetIds, options, command) => runAction(command, (context) => transferAssetsCommand(context, assetIds, parseOptions(transferOptionsSchema, options))));
17562
17976
  }
17563
17977
 
17978
+ // src/oauth/login.ts
17979
+ import { setTimeout as sleep } from "node:timers/promises";
17980
+
17981
+ // src/oauth/loopback.ts
17982
+ import { createServer } from "node:http";
17983
+ var CALLBACK_PATH = "/callback";
17984
+ var LOOPBACK_HOST = "127.0.0.1";
17985
+ var SUCCESS_PAGE = "<!doctype html><meta charset=utf-8><title>Raster CLI</title>" + '<body style="font-family:system-ui;text-align:center;padding-top:4rem">' + "<h1>Signed in</h1><p>You can close this tab and return to the terminal.</p></body>";
17986
+ var FAILURE_PAGE = "<!doctype html><meta charset=utf-8><title>Raster CLI</title>" + '<body style="font-family:system-ui;text-align:center;padding-top:4rem">' + "<h1>Sign-in failed</h1><p>You can close this tab and return to the terminal.</p></body>";
17987
+ async function startLoopbackServer() {
17988
+ let resolveCallback;
17989
+ let rejectCallback;
17990
+ const callbackPromise = new Promise((resolve, reject) => {
17991
+ resolveCallback = resolve;
17992
+ rejectCallback = reject;
17993
+ });
17994
+ let settled = false;
17995
+ const server = createServer((request, response) => {
17996
+ const url2 = new URL(request.url ?? "/", `http://${LOOPBACK_HOST}`);
17997
+ if (url2.pathname !== CALLBACK_PATH) {
17998
+ response.writeHead(404).end();
17999
+ return;
18000
+ }
18001
+ const code = url2.searchParams.get("code");
18002
+ const state = url2.searchParams.get("state");
18003
+ if (code && state) {
18004
+ response.writeHead(200, { "Content-Type": "text/html" }).end(SUCCESS_PAGE);
18005
+ if (!settled) {
18006
+ settled = true;
18007
+ resolveCallback({ code, state });
18008
+ }
18009
+ return;
18010
+ }
18011
+ response.writeHead(400, { "Content-Type": "text/html" }).end(FAILURE_PAGE);
18012
+ if (!settled) {
18013
+ settled = true;
18014
+ const error51 = url2.searchParams.get("error");
18015
+ rejectCallback(new CliError(error51 ? `The authorization server returned an error: ${error51}.` : "The authorization server returned no code.", EXIT_CODES.auth));
18016
+ }
18017
+ });
18018
+ let port;
18019
+ try {
18020
+ port = await listenOnEphemeralPort(server);
18021
+ } catch (error51) {
18022
+ server.close();
18023
+ throw error51;
18024
+ }
18025
+ return {
18026
+ redirectUri: `http://${LOOPBACK_HOST}:${port}${CALLBACK_PATH}`,
18027
+ waitForCallback: () => callbackPromise,
18028
+ close: () => {
18029
+ server.close();
18030
+ }
18031
+ };
18032
+ }
18033
+ function listenOnEphemeralPort(server) {
18034
+ return new Promise((resolve, reject) => {
18035
+ server.once("error", reject);
18036
+ server.listen(0, LOOPBACK_HOST, () => {
18037
+ const address = server.address();
18038
+ if (address !== null && typeof address === "object" && typeof address.port === "number") {
18039
+ resolve(address.port);
18040
+ return;
18041
+ }
18042
+ reject(new CliError("Could not bind a loopback port for the browser login.", EXIT_CODES.generic));
18043
+ });
18044
+ });
18045
+ }
18046
+
18047
+ // src/oauth/pkce.ts
18048
+ import { createHash, randomBytes } from "node:crypto";
18049
+ var VERIFIER_BYTES = 32;
18050
+ var STATE_BYTES = 16;
18051
+ function base64UrlEncode(input) {
18052
+ return input.toString("base64url");
18053
+ }
18054
+ function createPkce() {
18055
+ const verifier = base64UrlEncode(randomBytes(VERIFIER_BYTES));
18056
+ const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
18057
+ return { verifier, challenge, method: "S256" };
18058
+ }
18059
+ function createState() {
18060
+ return base64UrlEncode(randomBytes(STATE_BYTES));
18061
+ }
18062
+
18063
+ // src/oauth/browser.ts
18064
+ var DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60000;
18065
+ async function withDeadline(promise2, ms) {
18066
+ let timer;
18067
+ try {
18068
+ return await Promise.race([
18069
+ promise2,
18070
+ new Promise((_, reject) => {
18071
+ timer = setTimeout(() => reject(new CliError("Timed out waiting for browser sign-in. Run `raster auth login` again.", EXIT_CODES.auth)), ms);
18072
+ })
18073
+ ]);
18074
+ } finally {
18075
+ if (timer)
18076
+ clearTimeout(timer);
18077
+ }
18078
+ }
18079
+ async function runBrowserLogin(options) {
18080
+ const pkce = createPkce();
18081
+ const state = createState();
18082
+ const server = await startLoopbackServer();
18083
+ try {
18084
+ const authorizeUrl = buildAuthorizeUrl({
18085
+ metadata: options.metadata,
18086
+ clientId: options.clientId,
18087
+ redirectUri: server.redirectUri,
18088
+ challenge: pkce.challenge,
18089
+ state
18090
+ });
18091
+ await options.openBrowser(authorizeUrl);
18092
+ const callback = await withDeadline(server.waitForCallback(), options.loginTimeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS);
18093
+ if (callback.state !== state) {
18094
+ throw new CliError("The browser login response failed its state check. Run `raster auth login` again.", EXIT_CODES.auth);
18095
+ }
18096
+ const body = await postForm(options.metadata.tokenEndpoint, {
18097
+ grant_type: "authorization_code",
18098
+ code: callback.code,
18099
+ redirect_uri: server.redirectUri,
18100
+ client_id: options.clientId,
18101
+ code_verifier: pkce.verifier,
18102
+ resource: RESOURCE_INDICATOR
18103
+ }, options.fetch);
18104
+ return toStoredTokens(parseTokenResponse(body), {
18105
+ issuer: options.issuer,
18106
+ resource: RESOURCE_INDICATOR,
18107
+ now: options.now()
18108
+ });
18109
+ } finally {
18110
+ server.close();
18111
+ }
18112
+ }
18113
+ function buildAuthorizeUrl(options) {
18114
+ const url2 = new URL(options.metadata.authorizationEndpoint);
18115
+ url2.search = new URLSearchParams({
18116
+ response_type: "code",
18117
+ client_id: options.clientId,
18118
+ redirect_uri: options.redirectUri,
18119
+ scope: USER_SCOPE,
18120
+ resource: RESOURCE_INDICATOR,
18121
+ code_challenge: options.challenge,
18122
+ code_challenge_method: "S256",
18123
+ state: options.state
18124
+ }).toString();
18125
+ return url2.toString();
18126
+ }
18127
+
18128
+ // src/oauth/device.ts
18129
+ var SLOW_DOWN_INCREMENT_SECONDS = 5;
18130
+ var SECONDS_TO_MS2 = 1000;
18131
+ var deviceResponseSchema = exports_external.object({
18132
+ device_code: exports_external.string().min(1),
18133
+ user_code: exports_external.string().min(1),
18134
+ verification_uri: exports_external.url(),
18135
+ verification_uri_complete: exports_external.url().optional(),
18136
+ expires_in: exports_external.number().positive(),
18137
+ interval: exports_external.number().positive().optional()
18138
+ });
18139
+ var DEFAULT_INTERVAL_SECONDS = 5;
18140
+ async function requestDeviceCode(options) {
18141
+ const body = await postForm(options.metadata.deviceAuthorizationEndpoint, { client_id: options.clientId, scope: USER_SCOPE, resource: RESOURCE_INDICATOR }, options.fetch);
18142
+ const parsed = deviceResponseSchema.safeParse(body);
18143
+ if (!parsed.success) {
18144
+ throw new CliError("The device authorization response was malformed.", EXIT_CODES.auth);
18145
+ }
18146
+ return {
18147
+ deviceCode: parsed.data.device_code,
18148
+ userCode: parsed.data.user_code,
18149
+ verificationUri: parsed.data.verification_uri,
18150
+ verificationUriComplete: parsed.data.verification_uri_complete,
18151
+ expiresInSeconds: parsed.data.expires_in,
18152
+ intervalSeconds: parsed.data.interval ?? DEFAULT_INTERVAL_SECONDS
18153
+ };
18154
+ }
18155
+ function nextPollAction(errorCode, interval) {
18156
+ if (errorCode === "authorization_pending")
18157
+ return { kind: "wait", interval };
18158
+ if (errorCode === "slow_down")
18159
+ return { kind: "wait", interval: interval + SLOW_DOWN_INCREMENT_SECONDS };
18160
+ return { kind: "fail" };
18161
+ }
18162
+ async function pollForToken(options) {
18163
+ const deadline = options.now() + options.expiresInSeconds * SECONDS_TO_MS2;
18164
+ let interval = options.intervalSeconds;
18165
+ while (true) {
18166
+ if (options.now() >= deadline) {
18167
+ throw new CliError("The device code expired before you finished signing in. Run `raster auth login` again.", EXIT_CODES.auth);
18168
+ }
18169
+ let outcome;
18170
+ try {
18171
+ outcome = await pollOnce(options);
18172
+ } catch (error51) {
18173
+ if (!(error51 instanceof NetworkError))
18174
+ throw error51;
18175
+ outcome = { kind: "error", error: "authorization_pending" };
18176
+ }
18177
+ if (outcome.kind === "tokens")
18178
+ return outcome.tokens;
18179
+ const action = nextPollAction(outcome.error, interval);
18180
+ if (action.kind === "fail") {
18181
+ throw new CliError(`Device authorization failed: ${outcome.error}. Run \`raster auth login\` again.`, EXIT_CODES.auth);
18182
+ }
18183
+ interval = action.interval;
18184
+ await options.sleep(interval * SECONDS_TO_MS2);
18185
+ }
18186
+ }
18187
+ async function pollOnce(options) {
18188
+ try {
18189
+ const body = await postForm(options.metadata.tokenEndpoint, { grant_type: DEVICE_CODE_GRANT_TYPE, device_code: options.deviceCode, client_id: options.clientId }, options.fetch);
18190
+ const tokens = toStoredTokens(parseTokenResponse(body), {
18191
+ issuer: options.issuer,
18192
+ resource: RESOURCE_INDICATOR,
18193
+ now: options.now()
18194
+ });
18195
+ return { kind: "tokens", tokens };
18196
+ } catch (error51) {
18197
+ if (error51 instanceof OAuthError)
18198
+ return { kind: "error", error: error51.error };
18199
+ throw error51;
18200
+ }
18201
+ }
18202
+
18203
+ // src/oauth/login.ts
18204
+ async function runOAuthLogin(method, deps) {
18205
+ const metadata = await discover(deps.issuer, deps.fetch);
18206
+ const clientId = await ensureClientId({
18207
+ issuer: deps.issuer,
18208
+ metadata,
18209
+ env: deps.env,
18210
+ fetch: deps.fetch,
18211
+ pinnedClientId: deps.pinnedClientId
18212
+ });
18213
+ if (method === "browser") {
18214
+ if (!deps.openBrowser) {
18215
+ throw new CliError("No browser opener is available for the authorization-code flow.", EXIT_CODES.auth);
18216
+ }
18217
+ return runBrowserLogin({
18218
+ metadata,
18219
+ issuer: deps.issuer,
18220
+ clientId,
18221
+ fetch: deps.fetch,
18222
+ openBrowser: deps.openBrowser,
18223
+ now: deps.now
18224
+ });
18225
+ }
18226
+ const device = await requestDeviceCode({ metadata, clientId, fetch: deps.fetch });
18227
+ deps.onDevicePrompt?.(device);
18228
+ return pollForToken({
18229
+ metadata,
18230
+ issuer: deps.issuer,
18231
+ clientId,
18232
+ deviceCode: device.deviceCode,
18233
+ intervalSeconds: device.intervalSeconds,
18234
+ expiresInSeconds: device.expiresInSeconds,
18235
+ fetch: deps.fetch,
18236
+ sleep: deps.sleep ?? ((ms) => sleep(ms)),
18237
+ now: deps.now
18238
+ });
18239
+ }
18240
+
18241
+ // src/oauth/revoke.ts
18242
+ async function revokeToken(options) {
18243
+ try {
18244
+ const issuer = assertRasterOrLocalHost(options.tokens.issuer, "auth issuer");
18245
+ const metadata = await discover(issuer, options.fetch);
18246
+ const clientId = await ensureClientId({
18247
+ issuer,
18248
+ metadata,
18249
+ env: options.env,
18250
+ fetch: options.fetch,
18251
+ pinnedClientId: options.pinnedClientId
18252
+ });
18253
+ const response = await options.fetch(metadata.revocationEndpoint, {
18254
+ method: "POST",
18255
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
18256
+ body: new URLSearchParams({
18257
+ token: options.tokens.refreshToken,
18258
+ token_type_hint: "refresh_token",
18259
+ client_id: clientId
18260
+ }).toString(),
18261
+ signal: AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS)
18262
+ });
18263
+ return response.ok;
18264
+ } catch {
18265
+ return false;
18266
+ }
18267
+ }
18268
+
17564
18269
  // src/commands/auth.ts
17565
18270
  async function fetchViewer(context, apiKey) {
17566
18271
  const client = context.clientFactory({ apiKey });
17567
18272
  return unwrapResult(() => client.GET("/me", { params: { header: VERSION_HEADER } }));
17568
18273
  }
17569
- async function loginCommand(context) {
17570
- const flagKey = context.apiKey?.source === "flag" ? context.apiKey.key : null;
17571
- const key = flagKey ?? (context.isInteractive ? await context.promptSecret("Paste your Raster API key") : null);
17572
- if (!key) {
17573
- throw new UsageError("Provide a key with --api-key, or run `raster auth login` in an interactive terminal.");
18274
+ async function loginCommand(context, options = {}) {
18275
+ if (context.apiKey?.source === "flag") {
18276
+ await apiKeyLogin(context, context.apiKey.key);
18277
+ return;
18278
+ }
18279
+ if (!options.device && !options.browser && context.isInteractive) {
18280
+ const promptedKey = await context.promptSecret("Paste your Raster API key (or press Enter to use OAuth)");
18281
+ if (promptedKey) {
18282
+ await apiKeyLogin(context, promptedKey);
18283
+ return;
18284
+ }
18285
+ }
18286
+ if (context.env.RASTER_API_KEY && !context.isInteractive && !options.device && !options.browser) {
18287
+ await apiKeyLogin(context, context.env.RASTER_API_KEY);
18288
+ return;
17574
18289
  }
18290
+ await oauthLogin(context, options);
18291
+ }
18292
+ async function apiKeyLogin(context, key) {
17575
18293
  const viewer = await fetchViewer(context, key);
17576
18294
  const config2 = await readConfig(context.env);
17577
18295
  await writeConfig({
17578
18296
  ...config2,
17579
18297
  apiKey: key,
17580
18298
  organizationId: viewer?.organizationId,
17581
- libraries: (viewer?.libraries ?? []).filter((library) => typeof library === "string")
18299
+ libraries: (viewer?.libraries ?? []).map((entry) => entry.library),
18300
+ oauth: undefined
17582
18301
  }, context.env);
17583
18302
  if (context.json) {
17584
18303
  printJson(context, viewer);
@@ -17587,14 +18306,62 @@ async function loginCommand(context) {
17587
18306
  const organizationLabel = viewer?.organizationName ?? viewer?.organizationId ?? "your organization";
17588
18307
  note(context, `Logged in to ${organizationLabel} as ${maskApiKey(key)}. Key stored in ${configPath(context.env)}.`);
17589
18308
  }
18309
+ function chooseLoginMethod(context, options) {
18310
+ if (options.device && options.browser) {
18311
+ throw new UsageError("Pass only one of --device or --browser.");
18312
+ }
18313
+ if (options.browser) {
18314
+ if (!context.canOpenBrowser()) {
18315
+ throw new UsageError("No browser is available to open. Use --device, or run on a machine with a display.");
18316
+ }
18317
+ return "browser";
18318
+ }
18319
+ if (options.device)
18320
+ return "device";
18321
+ return context.isInteractive && context.canOpenBrowser() ? "browser" : "device";
18322
+ }
18323
+ async function oauthLogin(context, options) {
18324
+ const method = chooseLoginMethod(context, options);
18325
+ const tokens = await runOAuthLogin(method, {
18326
+ issuer: context.issuer,
18327
+ env: context.env,
18328
+ fetch: context.fetch,
18329
+ pinnedClientId: context.pinnedClientId,
18330
+ now: context.now,
18331
+ openBrowser: context.openBrowser,
18332
+ onDevicePrompt: (prompt) => {
18333
+ note(context, `To sign in, visit ${prompt.verificationUri} and enter the code: ${prompt.userCode}`);
18334
+ if (prompt.verificationUriComplete) {
18335
+ note(context, `Or open this link directly: ${prompt.verificationUriComplete}`);
18336
+ }
18337
+ note(context, "Waiting for you to finish signing in…");
18338
+ }
18339
+ });
18340
+ await writeOAuthTokens(tokens, context.env);
18341
+ if (context.json) {
18342
+ printJson(context, { issuer: tokens.issuer, scope: tokens.scope, resource: tokens.resource });
18343
+ return;
18344
+ }
18345
+ note(context, `Logged in via OAuth (${tokens.scope} scope). Tokens stored in ${configPath(context.env)}. ` + "Pass --org <id> or set RASTER_ORG to choose an organization; run `raster whoami` to list them.");
18346
+ }
17590
18347
  async function logoutCommand(context) {
17591
18348
  const config2 = await readConfig(context.env);
17592
- delete config2.apiKey;
17593
- await writeConfig(config2, context.env);
17594
- note(context, "Logged out. Stored API key removed.");
18349
+ if (config2.oauth) {
18350
+ await revokeToken({
18351
+ tokens: config2.oauth,
18352
+ env: context.env,
18353
+ fetch: context.fetch,
18354
+ pinnedClientId: context.pinnedClientId
18355
+ });
18356
+ }
18357
+ const cleared = { ...config2 };
18358
+ delete cleared.apiKey;
18359
+ delete cleared.oauth;
18360
+ await writeConfig(cleared, context.env);
18361
+ note(context, "Logged out. Stored credentials removed.");
17595
18362
  }
17596
18363
  async function statusCommand(context) {
17597
- if (!context.apiKey) {
18364
+ if (!context.credential) {
17598
18365
  if (context.json) {
17599
18366
  printJson(context, { authenticated: false });
17600
18367
  return;
@@ -17602,7 +18369,22 @@ async function statusCommand(context) {
17602
18369
  note(context, "Not authenticated. Run `raster auth login` or set RASTER_API_KEY.");
17603
18370
  return;
17604
18371
  }
17605
- const { key, source } = context.apiKey;
18372
+ if (context.credential.kind === "oauth") {
18373
+ statusForOAuth(context, context.credential.tokens);
18374
+ return;
18375
+ }
18376
+ await statusForApiKey(context, context.credential.key, context.credential.source);
18377
+ }
18378
+ function statusForOAuth(context, tokens) {
18379
+ const fresh = isAccessTokenFresh(tokens, context.now());
18380
+ if (context.json) {
18381
+ printJson(context, { authenticated: true, kind: "oauth", issuer: tokens.issuer, scope: tokens.scope, fresh });
18382
+ return;
18383
+ }
18384
+ note(context, `OAuth login on ${tokens.issuer} (${tokens.scope} scope).`);
18385
+ note(context, fresh ? "Access token is fresh." : "Access token will be refreshed on the next request.");
18386
+ }
18387
+ async function statusForApiKey(context, key, source) {
17606
18388
  let viewer = null;
17607
18389
  let failureMessage = null;
17608
18390
  try {
@@ -17611,12 +18393,7 @@ async function statusCommand(context) {
17611
18393
  failureMessage = error51 instanceof Error ? error51.message : String(error51);
17612
18394
  }
17613
18395
  if (context.json) {
17614
- printJson(context, {
17615
- authenticated: true,
17616
- source,
17617
- keyPrefix: maskApiKey(key),
17618
- organization: viewer ?? null
17619
- });
18396
+ printJson(context, { authenticated: true, kind: "apiKey", source, keyPrefix: maskApiKey(key), organization: viewer ?? null });
17620
18397
  return;
17621
18398
  }
17622
18399
  note(context, `API key ${maskApiKey(key)} (from ${source}).`);
@@ -17627,25 +18404,34 @@ async function statusCommand(context) {
17627
18404
  note(context, `Could not validate the key against the API: ${failureMessage ?? "unknown error"}`);
17628
18405
  }
17629
18406
  async function whoamiCommand(context) {
17630
- const { key } = requireApiKey(context);
17631
- const viewer = await fetchViewer(context, key);
18407
+ requireAuth(context);
18408
+ const viewer = await unwrapResult(() => context.clientFactory().GET("/me", { params: { header: VERSION_HEADER } }));
17632
18409
  if (context.json) {
17633
18410
  printJson(context, viewer);
17634
18411
  return;
17635
18412
  }
18413
+ const isBroadOAuthLogin = context.credential?.kind === "oauth" && context.credential.tokens.scope !== "org";
18414
+ const organizations = viewer?.organizations ?? [];
18415
+ if (isBroadOAuthLogin && organizations.length > 0) {
18416
+ renderTable(context, organizations, [
18417
+ { header: "organization", value: (organization) => organization.organizationId },
18418
+ { header: "name", value: (organization) => organization.organizationName ?? "" }
18419
+ ]);
18420
+ return;
18421
+ }
17636
18422
  renderRecord(context, [
17637
18423
  { label: "organization", value: viewer?.organizationId ?? "" },
17638
18424
  { label: "name", value: viewer?.organizationName ?? "" },
17639
18425
  { label: "plan", value: viewer?.plan ?? "" },
17640
- { label: "libraries", value: (viewer?.libraries ?? []).join(", ") }
18426
+ { label: "libraries", value: (viewer?.libraries ?? []).map((entry) => entry.library).join(", ") }
17641
18427
  ]);
17642
18428
  }
17643
18429
  function registerAuthCommands(program2, runAction) {
17644
18430
  const auth = program2.command("auth").description("Manage CLI authentication");
17645
- auth.command("login").description("Validate an API key against the API and store it (pass --api-key, or run interactively to be prompted)").action((_options, command) => runAction(command, (context) => loginCommand(context)));
17646
- auth.command("logout").description("Remove the stored API key").action((_options, command) => runAction(command, (context) => logoutCommand(context)));
17647
- auth.command("status").description("Show which API key is in use and where it came from").action((_options, command) => runAction(command, (context) => statusCommand(context)));
17648
- program2.command("whoami").description("Show the organization and libraries the API key can access").action((_options, command) => runAction(command, (context) => whoamiCommand(context)));
18431
+ auth.command("login").description("Sign in via OAuth (opens a browser when available, otherwise a device code; force with --device/--browser), or validate an API key with --api-key").option("--device", "Force the device authorization grant (SSH/headless friendly)").option("--browser", "Use the browser authorization-code flow").action((options, command) => runAction(command, (context) => loginCommand(context, { device: options.device, browser: options.browser })));
18432
+ auth.command("logout").description("Revoke the OAuth token (if any) and remove stored credentials").action((_options, command) => runAction(command, (context) => logoutCommand(context)));
18433
+ auth.command("status").description("Show which credential is in use and where it came from").action((_options, command) => runAction(command, (context) => statusCommand(context)));
18434
+ program2.command("whoami").description("Show the organizations and libraries the current credential can access").action((_options, command) => runAction(command, (context) => whoamiCommand(context)));
17649
18435
  }
17650
18436
 
17651
18437
  // src/commands/libraries.ts
@@ -17661,7 +18447,7 @@ var listOptionsSchema2 = exports_external.object({
17661
18447
  pageSize: exports_external.coerce.number().int().min(1).optional()
17662
18448
  });
17663
18449
  async function listLibrariesCommand(context, options) {
17664
- requireApiKey(context);
18450
+ requireAuth(context);
17665
18451
  const organizationId = await context.resolveOrg();
17666
18452
  const client = context.clientFactory();
17667
18453
  const libraries = await unwrapResult(() => client.GET("/organizations/{organizationId}/libraries", {
@@ -17682,7 +18468,7 @@ var createOptionsSchema = exports_external.object({
17682
18468
  slug: exports_external.string().min(1).optional()
17683
18469
  });
17684
18470
  async function createLibraryCommand(context, options) {
17685
- requireApiKey(context);
18471
+ requireAuth(context);
17686
18472
  const organizationId = await context.resolveOrg();
17687
18473
  const client = context.clientFactory();
17688
18474
  const library = await unwrapResult(() => client.POST("/organizations/{organizationId}/libraries", {
@@ -17703,7 +18489,7 @@ var renameOptionsSchema = exports_external.object({
17703
18489
  name: exports_external.string().min(1)
17704
18490
  });
17705
18491
  async function renameLibraryCommand(context, options) {
17706
- requireApiKey(context);
18492
+ requireAuth(context);
17707
18493
  const organizationId = await context.resolveOrg();
17708
18494
  const libraryId = await context.resolveLibrary();
17709
18495
  const client = context.clientFactory();
@@ -17779,7 +18565,7 @@ var listOptionsSchema3 = exports_external.object({
17779
18565
  limit: exports_external.coerce.number().int().min(1).optional()
17780
18566
  });
17781
18567
  async function listTagsCommand(context, options) {
17782
- requireApiKey(context);
18568
+ requireAuth(context);
17783
18569
  const organizationId = await context.resolveOrg();
17784
18570
  const libraryId = await context.resolveLibrary();
17785
18571
  const client = context.clientFactory();
@@ -17802,7 +18588,7 @@ var mutateOptionsSchema = exports_external.object({
17802
18588
  async function tagAssetsCommand(context, assetIds, options) {
17803
18589
  if (assetIds.length === 0)
17804
18590
  throw new UsageError("Provide at least one asset id.");
17805
- requireApiKey(context);
18591
+ requireAuth(context);
17806
18592
  const organizationId = await context.resolveOrg();
17807
18593
  const libraryId = await context.resolveLibrary();
17808
18594
  const client = context.clientFactory();
@@ -17819,7 +18605,7 @@ async function tagAssetsCommand(context, assetIds, options) {
17819
18605
  async function untagAssetsCommand(context, assetIds, options) {
17820
18606
  if (assetIds.length === 0)
17821
18607
  throw new UsageError("Provide at least one asset id.");
17822
- requireApiKey(context);
18608
+ requireAuth(context);
17823
18609
  const organizationId = await context.resolveOrg();
17824
18610
  const libraryId = await context.resolveLibrary();
17825
18611
  const client = context.clientFactory();