@raster-app/cli 0.2.0 → 0.3.1

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 +883 -84
  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.1",
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,17 +17088,348 @@ 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
 
17083
17422
  // src/lib/output.ts
17423
+ var ANSI = { reset: "\x1B[0m", bold: "\x1B[1m", dim: "\x1B[2m", cyan: "\x1B[36m" };
17424
+ function paint(text, code, enabled) {
17425
+ return enabled ? `${code}${text}${ANSI.reset}` : text;
17426
+ }
17427
+ function bold(text, enabled) {
17428
+ return paint(text, ANSI.bold, enabled);
17429
+ }
17430
+ function dim(text, enabled) {
17431
+ return paint(text, ANSI.dim, enabled);
17432
+ }
17084
17433
  function printJson(output, payload) {
17085
17434
  output.stdout(`${JSON.stringify(payload, null, 2)}
17086
17435
  `);
@@ -17193,73 +17542,144 @@ function promptSecret(message) {
17193
17542
  }
17194
17543
 
17195
17544
  // src/lib/context.ts
17545
+ var RASTER_ORG_ENV = "RASTER_ORG";
17546
+ var PINNED_CLIENT_ID_ENV = "RASTER_OAUTH_CLIENT_ID";
17547
+ function toLibraryIds(libraries) {
17548
+ return (libraries ?? []).map((entry) => typeof entry === "string" ? entry : entry?.library).filter((id) => typeof id === "string");
17549
+ }
17196
17550
  async function createCommandContext(globals, overrides = {}) {
17197
17551
  const env = overrides.env ?? process.env;
17198
17552
  const config2 = await readConfig(env);
17199
- const apiKey = resolveApiKey({ flagKey: globals.apiKey, env, config: config2 });
17553
+ const now = overrides.now ?? Date.now;
17200
17554
  const output = {
17201
17555
  json: globals.json === true,
17202
17556
  verbose: globals.verbose === true,
17557
+ color: overrides.stderr === undefined && process.stderr.isTTY === true && env.NO_COLOR === undefined,
17203
17558
  stdout: overrides.stdout ?? ((text) => void process.stdout.write(text)),
17204
17559
  stderr: overrides.stderr ?? ((text) => void process.stderr.write(text))
17205
17560
  };
17206
17561
  const baseUrl = validateBaseUrl(env.RASTER_API_BASE_URL ?? DEFAULT_BASE_URL);
17207
17562
  const baseFetch = overrides.fetch ?? globalThis.fetch;
17208
- const instrumentedFetch = instrumentFetch(baseFetch, output, apiKey ? maskApiKey(apiKey.key) : null);
17563
+ const credential = await resolveActiveCredential({
17564
+ flagKey: globals.apiKey,
17565
+ env,
17566
+ config: config2,
17567
+ fetch: instrumentFetch(baseFetch, output, null),
17568
+ now: now()
17569
+ });
17570
+ const apiKey = credential?.kind === "apiKey" ? { key: credential.key, source: credential.source } : null;
17571
+ const bearer = credentialBearer(credential);
17572
+ const instrumentedFetch = instrumentFetch(baseFetch, output, bearer ? maskApiKey(bearer) : null);
17209
17573
  const clientFactory = (clientOverrides) => createApiClient({
17210
17574
  baseUrl,
17211
- apiKey: clientOverrides ? clientOverrides.apiKey : apiKey?.key ?? null,
17575
+ apiKey: clientOverrides ? clientOverrides.apiKey : bearer,
17212
17576
  fetch: instrumentedFetch
17213
17577
  });
17214
17578
  let viewerPromise = null;
17215
17579
  const loadViewer = () => {
17216
17580
  if (!viewerPromise) {
17217
- if (!apiKey) {
17218
- throw new AuthSetupError("No API key found. Run `raster auth login` or set RASTER_API_KEY.");
17581
+ if (!credential) {
17582
+ throw new AuthSetupError("Not authenticated. Run `raster auth login` or set RASTER_API_KEY.");
17219
17583
  }
17220
- viewerPromise = unwrapResult(() => clientFactory({ apiKey: apiKey.key }).GET("/me", { params: { header: VERSION_HEADER } }));
17584
+ viewerPromise = unwrapResult(() => clientFactory().GET("/me", { params: { header: VERSION_HEADER } }));
17221
17585
  }
17222
17586
  return viewerPromise;
17223
17587
  };
17224
17588
  const cacheMatchesKey = config2.apiKey !== undefined && apiKey?.key === config2.apiKey;
17589
+ const resolveOrgId = async () => {
17590
+ if (globals.org)
17591
+ return globals.org;
17592
+ if (env[RASTER_ORG_ENV])
17593
+ return env[RASTER_ORG_ENV];
17594
+ if (credential?.kind === "oauth") {
17595
+ if (credential.tokens.scope === "org") {
17596
+ const viewer2 = await loadViewer();
17597
+ if (viewer2.organizationId)
17598
+ return viewer2.organizationId;
17599
+ throw new CliError("This login is bound to an organization that is no longer available. Run `raster auth login` again.", EXIT_CODES.auth);
17600
+ }
17601
+ if (config2.defaultOrg)
17602
+ return config2.defaultOrg;
17603
+ throw new UsageError("This login can access multiple organizations. Pass --org <id> (or set RASTER_ORG); run `raster whoami` to list them.");
17604
+ }
17605
+ if (cacheMatchesKey && config2.organizationId)
17606
+ return config2.organizationId;
17607
+ const viewer = await loadViewer();
17608
+ if (!viewer.organizationId) {
17609
+ throw new CliError("Could not determine the organization from the API key.", EXIT_CODES.generic);
17610
+ }
17611
+ return viewer.organizationId;
17612
+ };
17613
+ const librariesForTarget = async () => {
17614
+ const cached2 = cacheMatchesKey ? config2.libraries : undefined;
17615
+ if (cached2)
17616
+ return cached2;
17617
+ const viewer = await loadViewer();
17618
+ if (credential?.kind === "oauth" && credential.tokens.scope !== "org") {
17619
+ const organizationId = await resolveOrgId();
17620
+ const entry = viewer.organizations?.find((organization) => organization?.organizationId === organizationId);
17621
+ return entry?.libraries ?? [];
17622
+ }
17623
+ return viewer.libraries ?? [];
17624
+ };
17225
17625
  return {
17226
17626
  ...output,
17227
17627
  env,
17228
17628
  baseUrl,
17229
17629
  isInteractive: overrides.isInteractive ?? process.stdin.isTTY === true,
17630
+ credential,
17230
17631
  apiKey,
17632
+ bearerToken: bearer,
17231
17633
  fetch: instrumentedFetch,
17232
17634
  clientFactory,
17233
17635
  confirm: overrides.confirm ?? promptConfirm,
17234
17636
  promptSecret: overrides.promptSecret ?? promptSecret,
17637
+ now,
17638
+ issuer: resolveIssuer({ env, config: config2 }),
17639
+ pinnedClientId: env[PINNED_CLIENT_ID_ENV],
17640
+ openBrowser: overrides.openBrowser ?? openBrowser,
17641
+ canOpenBrowser: overrides.canOpenBrowser ?? canOpenBrowser,
17235
17642
  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
- },
17643
+ resolveOrg: resolveOrgId,
17247
17644
  resolveLibrary: async () => {
17248
17645
  if (globals.library)
17249
17646
  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");
17647
+ const libraries = toLibraryIds(await librariesForTarget());
17253
17648
  const onlyLibrary = libraries[0];
17254
17649
  if (libraries.length === 1 && onlyLibrary)
17255
17650
  return onlyLibrary;
17256
17651
  if (libraries.length === 0) {
17257
- throw new AuthSetupError("The API key has no library access.");
17652
+ throw new AuthSetupError("No library access for this login. Pass --library <id>.");
17258
17653
  }
17259
- throw new UsageError(`The API key can access ${libraries.length} libraries (${libraries.join(", ")}). Pass --library <id>.`);
17654
+ throw new UsageError(`This login can access ${libraries.length} libraries (${libraries.join(", ")}). Pass --library <id>.`);
17260
17655
  }
17261
17656
  };
17262
17657
  }
17658
+ function credentialBearer(credential) {
17659
+ if (!credential)
17660
+ return null;
17661
+ return credential.kind === "apiKey" ? credential.key : credential.tokens.accessToken;
17662
+ }
17663
+ async function resolveActiveCredential(options) {
17664
+ const credential = resolveCredential({ flagKey: options.flagKey, env: options.env, config: options.config });
17665
+ if (credential?.kind !== "oauth")
17666
+ return credential;
17667
+ const session = await ensureFreshOAuthSession({
17668
+ tokens: credential.tokens,
17669
+ env: options.env,
17670
+ fetch: options.fetch,
17671
+ pinnedClientId: options.env[PINNED_CLIENT_ID_ENV],
17672
+ now: options.now
17673
+ });
17674
+ if (session.status === "reauth-required") {
17675
+ return resolveCredential({
17676
+ flagKey: options.flagKey,
17677
+ env: options.env,
17678
+ config: { ...options.config, oauth: undefined }
17679
+ });
17680
+ }
17681
+ return { kind: "oauth", tokens: session.tokens };
17682
+ }
17263
17683
  function instrumentFetch(baseFetch, output, maskedKey) {
17264
17684
  return async (input, init) => {
17265
17685
  const request = input instanceof Request ? input : new Request(String(input), init);
@@ -17276,10 +17696,15 @@ function instrumentFetch(baseFetch, output, maskedKey) {
17276
17696
  }
17277
17697
  };
17278
17698
  }
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.");
17699
+ function requireAuth(context) {
17700
+ if (context.credential)
17701
+ return context.credential;
17702
+ throw new AuthSetupError("Not authenticated. Run `raster auth login` or set RASTER_API_KEY.");
17703
+ }
17704
+ function requireBearer(context) {
17705
+ if (context.bearerToken)
17706
+ return context.bearerToken;
17707
+ throw new AuthSetupError("Not authenticated. Run `raster auth login` or set RASTER_API_KEY.");
17283
17708
  }
17284
17709
  async function executeCommand(context, action) {
17285
17710
  try {
@@ -17333,7 +17758,7 @@ async function listAssetsCommand(context, options) {
17333
17758
  if (options.tag.length > MAX_TAGS_PER_QUERY) {
17334
17759
  throw new ValidationError(`At most ${MAX_TAGS_PER_QUERY} --tag filters are supported per query.`);
17335
17760
  }
17336
- requireApiKey(context);
17761
+ requireAuth(context);
17337
17762
  const organizationId = await context.resolveOrg();
17338
17763
  const libraryId = await context.resolveLibrary();
17339
17764
  const client = context.clientFactory();
@@ -17355,7 +17780,7 @@ async function listAssetsCommand(context, options) {
17355
17780
  renderTable(context, assets, ASSET_COLUMNS);
17356
17781
  }
17357
17782
  async function getAssetCommand(context, assetId) {
17358
- requireApiKey(context);
17783
+ requireAuth(context);
17359
17784
  const asset = await fetchAsset(context, assetId);
17360
17785
  if (context.json) {
17361
17786
  printJson(context, asset);
@@ -17385,7 +17810,7 @@ var searchOptionsSchema = exports_external.object({
17385
17810
  pageSize: exports_external.coerce.number().int().min(1).optional()
17386
17811
  });
17387
17812
  async function searchAssetsCommand(context, query, options) {
17388
- requireApiKey(context);
17813
+ requireAuth(context);
17389
17814
  const organizationId = await context.resolveOrg();
17390
17815
  const client = context.clientFactory();
17391
17816
  const result = await unwrapResult(() => client.GET("/organizations/{organizationId}/search/assets", {
@@ -17412,7 +17837,7 @@ var downloadOptionsSchema = exports_external.object({
17412
17837
  force: exports_external.boolean().default(false)
17413
17838
  });
17414
17839
  async function downloadAssetCommand(context, assetId, options) {
17415
- requireApiKey(context);
17840
+ requireAuth(context);
17416
17841
  const asset = await fetchAsset(context, assetId);
17417
17842
  const fileUrl = asset.url;
17418
17843
  if (!fileUrl)
@@ -17440,7 +17865,7 @@ var uploadedAssetsSchema = exports_external.object({
17440
17865
  async function uploadAssetsCommand(context, files) {
17441
17866
  if (files.length === 0)
17442
17867
  throw new UsageError("Provide at least one file to upload.");
17443
- const { key } = requireApiKey(context);
17868
+ const bearer = requireBearer(context);
17444
17869
  const organizationId = await context.resolveOrg();
17445
17870
  const libraryId = await context.resolveLibrary();
17446
17871
  const batches = chunk(files, MAX_UPLOAD_FILES);
@@ -17460,7 +17885,7 @@ async function uploadAssetsCommand(context, files) {
17460
17885
  if (batches.length > 1) {
17461
17886
  note(context, `Uploading batch ${batchIndex + 1}/${batches.length} (${batch.length} files)…`);
17462
17887
  }
17463
- const response = await context.fetch(`${context.baseUrl}/organizations/${organizationId}/libraries/${libraryId}/assets`, { method: "POST", headers: buildRequestHeaders(key), body: formData });
17888
+ const response = await context.fetch(`${context.baseUrl}/organizations/${organizationId}/libraries/${libraryId}/assets`, { method: "POST", headers: buildRequestHeaders(bearer), body: formData });
17464
17889
  const payload = await parseEnvelope(response);
17465
17890
  payloads.push(payload);
17466
17891
  const parsed = uploadedAssetsSchema.safeParse(payload);
@@ -17487,7 +17912,7 @@ async function removeAssetsCommand(context, assetIds, options) {
17487
17912
  if (assetIds.length > MAX_DELETE_IDS) {
17488
17913
  throw new ValidationError(`At most ${MAX_DELETE_IDS} asset ids are supported per call.`);
17489
17914
  }
17490
- requireApiKey(context);
17915
+ requireAuth(context);
17491
17916
  const organizationId = await context.resolveOrg();
17492
17917
  const libraryId = await context.resolveLibrary();
17493
17918
  if (!options.yes && context.isInteractive) {
@@ -17506,13 +17931,13 @@ async function removeAssetsCommand(context, assetIds, options) {
17506
17931
  printJson(context, result);
17507
17932
  return;
17508
17933
  }
17509
- note(context, `Moved ${assetIds.length} asset(s) to trash. They are recoverable from the library trash.`);
17934
+ note(context, `Moved ${assetIds.length} asset(s) to trash.`);
17510
17935
  }
17511
17936
  var describeOptionsSchema = exports_external.object({
17512
17937
  text: exports_external.string().min(1)
17513
17938
  });
17514
17939
  async function describeAssetCommand(context, assetId, options) {
17515
- requireApiKey(context);
17940
+ requireAuth(context);
17516
17941
  const organizationId = await context.resolveOrg();
17517
17942
  const libraryId = await context.resolveLibrary();
17518
17943
  const client = context.clientFactory();
@@ -17535,7 +17960,7 @@ var transferOptionsSchema = exports_external.object({
17535
17960
  async function transferAssetsCommand(context, assetIds, options) {
17536
17961
  if (assetIds.length === 0)
17537
17962
  throw new UsageError("Provide at least one asset id.");
17538
- requireApiKey(context);
17963
+ requireAuth(context);
17539
17964
  const organizationId = await context.resolveOrg();
17540
17965
  const libraryId = await context.resolveLibrary();
17541
17966
  const client = context.clientFactory();
@@ -17561,48 +17986,418 @@ function registerAssetCommands(program2, runAction) {
17561
17986
  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
17987
  }
17563
17988
 
17989
+ // src/oauth/login.ts
17990
+ import { setTimeout as sleep } from "node:timers/promises";
17991
+
17992
+ // src/oauth/loopback.ts
17993
+ import { createServer } from "node:http";
17994
+ var CALLBACK_PATH = "/callback";
17995
+ var LOOPBACK_HOST = "127.0.0.1";
17996
+ 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>";
17997
+ 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>";
17998
+ async function startLoopbackServer() {
17999
+ let resolveCallback;
18000
+ let rejectCallback;
18001
+ const callbackPromise = new Promise((resolve, reject) => {
18002
+ resolveCallback = resolve;
18003
+ rejectCallback = reject;
18004
+ });
18005
+ let settled = false;
18006
+ const server = createServer((request, response) => {
18007
+ const url2 = new URL(request.url ?? "/", `http://${LOOPBACK_HOST}`);
18008
+ if (url2.pathname !== CALLBACK_PATH) {
18009
+ response.writeHead(404).end();
18010
+ return;
18011
+ }
18012
+ const code = url2.searchParams.get("code");
18013
+ const state = url2.searchParams.get("state");
18014
+ if (code && state) {
18015
+ response.writeHead(200, { "Content-Type": "text/html", Connection: "close" }).end(SUCCESS_PAGE);
18016
+ if (!settled) {
18017
+ settled = true;
18018
+ resolveCallback({ code, state });
18019
+ }
18020
+ return;
18021
+ }
18022
+ response.writeHead(400, { "Content-Type": "text/html", Connection: "close" }).end(FAILURE_PAGE);
18023
+ if (!settled) {
18024
+ settled = true;
18025
+ const error51 = url2.searchParams.get("error");
18026
+ rejectCallback(new CliError(error51 ? `The authorization server returned an error: ${error51}.` : "The authorization server returned no code.", EXIT_CODES.auth));
18027
+ }
18028
+ });
18029
+ let port;
18030
+ try {
18031
+ port = await listenOnEphemeralPort(server);
18032
+ } catch (error51) {
18033
+ server.close();
18034
+ throw error51;
18035
+ }
18036
+ return {
18037
+ redirectUri: `http://${LOOPBACK_HOST}:${port}${CALLBACK_PATH}`,
18038
+ waitForCallback: () => callbackPromise,
18039
+ close: () => {
18040
+ server.close();
18041
+ server.closeAllConnections?.();
18042
+ }
18043
+ };
18044
+ }
18045
+ function listenOnEphemeralPort(server) {
18046
+ return new Promise((resolve, reject) => {
18047
+ server.once("error", reject);
18048
+ server.listen(0, LOOPBACK_HOST, () => {
18049
+ const address = server.address();
18050
+ if (address !== null && typeof address === "object" && typeof address.port === "number") {
18051
+ resolve(address.port);
18052
+ return;
18053
+ }
18054
+ reject(new CliError("Could not bind a loopback port for the browser login.", EXIT_CODES.generic));
18055
+ });
18056
+ });
18057
+ }
18058
+
18059
+ // src/oauth/pkce.ts
18060
+ import { createHash, randomBytes } from "node:crypto";
18061
+ var VERIFIER_BYTES = 32;
18062
+ var STATE_BYTES = 16;
18063
+ function base64UrlEncode(input) {
18064
+ return input.toString("base64url");
18065
+ }
18066
+ function createPkce() {
18067
+ const verifier = base64UrlEncode(randomBytes(VERIFIER_BYTES));
18068
+ const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
18069
+ return { verifier, challenge, method: "S256" };
18070
+ }
18071
+ function createState() {
18072
+ return base64UrlEncode(randomBytes(STATE_BYTES));
18073
+ }
18074
+
18075
+ // src/oauth/browser.ts
18076
+ var DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60000;
18077
+ async function withDeadline(promise2, ms) {
18078
+ let timer;
18079
+ try {
18080
+ return await Promise.race([
18081
+ promise2,
18082
+ new Promise((_, reject) => {
18083
+ timer = setTimeout(() => reject(new CliError("Timed out waiting for browser sign-in. Run `raster auth login` again.", EXIT_CODES.auth)), ms);
18084
+ })
18085
+ ]);
18086
+ } finally {
18087
+ if (timer)
18088
+ clearTimeout(timer);
18089
+ }
18090
+ }
18091
+ async function runBrowserLogin(options) {
18092
+ const pkce = createPkce();
18093
+ const state = createState();
18094
+ const server = await startLoopbackServer();
18095
+ try {
18096
+ const authorizeUrl = buildAuthorizeUrl({
18097
+ metadata: options.metadata,
18098
+ clientId: options.clientId,
18099
+ redirectUri: server.redirectUri,
18100
+ challenge: pkce.challenge,
18101
+ state
18102
+ });
18103
+ options.onAuthorizeUrl?.(authorizeUrl);
18104
+ await options.openBrowser(authorizeUrl);
18105
+ const callback = await withDeadline(server.waitForCallback(), options.loginTimeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS);
18106
+ if (callback.state !== state) {
18107
+ throw new CliError("The browser login response failed its state check. Run `raster auth login` again.", EXIT_CODES.auth);
18108
+ }
18109
+ const body = await postForm(options.metadata.tokenEndpoint, {
18110
+ grant_type: "authorization_code",
18111
+ code: callback.code,
18112
+ redirect_uri: server.redirectUri,
18113
+ client_id: options.clientId,
18114
+ code_verifier: pkce.verifier,
18115
+ resource: RESOURCE_INDICATOR
18116
+ }, options.fetch);
18117
+ return toStoredTokens(parseTokenResponse(body), {
18118
+ issuer: options.issuer,
18119
+ resource: RESOURCE_INDICATOR,
18120
+ now: options.now()
18121
+ });
18122
+ } finally {
18123
+ server.close();
18124
+ }
18125
+ }
18126
+ function buildAuthorizeUrl(options) {
18127
+ const url2 = new URL(options.metadata.authorizationEndpoint);
18128
+ url2.search = new URLSearchParams({
18129
+ response_type: "code",
18130
+ client_id: options.clientId,
18131
+ redirect_uri: options.redirectUri,
18132
+ scope: USER_SCOPE,
18133
+ resource: RESOURCE_INDICATOR,
18134
+ code_challenge: options.challenge,
18135
+ code_challenge_method: "S256",
18136
+ state: options.state
18137
+ }).toString();
18138
+ return url2.toString();
18139
+ }
18140
+
18141
+ // src/oauth/device.ts
18142
+ var SLOW_DOWN_INCREMENT_SECONDS = 5;
18143
+ var SECONDS_TO_MS2 = 1000;
18144
+ var deviceResponseSchema = exports_external.object({
18145
+ device_code: exports_external.string().min(1),
18146
+ user_code: exports_external.string().min(1),
18147
+ verification_uri: exports_external.url(),
18148
+ verification_uri_complete: exports_external.url().optional(),
18149
+ expires_in: exports_external.number().positive(),
18150
+ interval: exports_external.number().positive().optional()
18151
+ });
18152
+ var DEFAULT_INTERVAL_SECONDS = 5;
18153
+ async function requestDeviceCode(options) {
18154
+ const body = await postForm(options.metadata.deviceAuthorizationEndpoint, { client_id: options.clientId, scope: USER_SCOPE, resource: RESOURCE_INDICATOR }, options.fetch);
18155
+ const parsed = deviceResponseSchema.safeParse(body);
18156
+ if (!parsed.success) {
18157
+ throw new CliError("The device authorization response was malformed.", EXIT_CODES.auth);
18158
+ }
18159
+ return {
18160
+ deviceCode: parsed.data.device_code,
18161
+ userCode: parsed.data.user_code,
18162
+ verificationUri: parsed.data.verification_uri,
18163
+ verificationUriComplete: parsed.data.verification_uri_complete,
18164
+ expiresInSeconds: parsed.data.expires_in,
18165
+ intervalSeconds: parsed.data.interval ?? DEFAULT_INTERVAL_SECONDS
18166
+ };
18167
+ }
18168
+ function nextPollAction(errorCode, interval) {
18169
+ if (errorCode === "authorization_pending")
18170
+ return { kind: "wait", interval };
18171
+ if (errorCode === "slow_down")
18172
+ return { kind: "wait", interval: interval + SLOW_DOWN_INCREMENT_SECONDS };
18173
+ return { kind: "fail" };
18174
+ }
18175
+ async function pollForToken(options) {
18176
+ const deadline = options.now() + options.expiresInSeconds * SECONDS_TO_MS2;
18177
+ let interval = options.intervalSeconds;
18178
+ while (true) {
18179
+ if (options.now() >= deadline) {
18180
+ throw new CliError("The device code expired before you finished signing in. Run `raster auth login` again.", EXIT_CODES.auth);
18181
+ }
18182
+ let outcome;
18183
+ try {
18184
+ outcome = await pollOnce(options);
18185
+ } catch (error51) {
18186
+ if (!(error51 instanceof NetworkError))
18187
+ throw error51;
18188
+ outcome = { kind: "error", error: "authorization_pending" };
18189
+ }
18190
+ if (outcome.kind === "tokens")
18191
+ return outcome.tokens;
18192
+ const action = nextPollAction(outcome.error, interval);
18193
+ if (action.kind === "fail") {
18194
+ throw new CliError(`Device authorization failed: ${outcome.error}. Run \`raster auth login\` again.`, EXIT_CODES.auth);
18195
+ }
18196
+ interval = action.interval;
18197
+ await options.sleep(interval * SECONDS_TO_MS2);
18198
+ }
18199
+ }
18200
+ async function pollOnce(options) {
18201
+ try {
18202
+ const body = await postForm(options.metadata.tokenEndpoint, { grant_type: DEVICE_CODE_GRANT_TYPE, device_code: options.deviceCode, client_id: options.clientId }, options.fetch);
18203
+ const tokens = toStoredTokens(parseTokenResponse(body), {
18204
+ issuer: options.issuer,
18205
+ resource: RESOURCE_INDICATOR,
18206
+ now: options.now()
18207
+ });
18208
+ return { kind: "tokens", tokens };
18209
+ } catch (error51) {
18210
+ if (error51 instanceof OAuthError)
18211
+ return { kind: "error", error: error51.error };
18212
+ throw error51;
18213
+ }
18214
+ }
18215
+
18216
+ // src/oauth/login.ts
18217
+ async function runOAuthLogin(method, deps) {
18218
+ const metadata = await discover(deps.issuer, deps.fetch);
18219
+ const clientId = await ensureClientId({
18220
+ issuer: deps.issuer,
18221
+ metadata,
18222
+ env: deps.env,
18223
+ fetch: deps.fetch,
18224
+ pinnedClientId: deps.pinnedClientId
18225
+ });
18226
+ if (method === "browser") {
18227
+ if (!deps.openBrowser) {
18228
+ throw new CliError("No browser opener is available for the authorization-code flow.", EXIT_CODES.auth);
18229
+ }
18230
+ return runBrowserLogin({
18231
+ metadata,
18232
+ issuer: deps.issuer,
18233
+ clientId,
18234
+ fetch: deps.fetch,
18235
+ openBrowser: deps.openBrowser,
18236
+ now: deps.now,
18237
+ onAuthorizeUrl: deps.onAuthorizeUrl
18238
+ });
18239
+ }
18240
+ const device = await requestDeviceCode({ metadata, clientId, fetch: deps.fetch });
18241
+ deps.onDevicePrompt?.(device);
18242
+ return pollForToken({
18243
+ metadata,
18244
+ issuer: deps.issuer,
18245
+ clientId,
18246
+ deviceCode: device.deviceCode,
18247
+ intervalSeconds: device.intervalSeconds,
18248
+ expiresInSeconds: device.expiresInSeconds,
18249
+ fetch: deps.fetch,
18250
+ sleep: deps.sleep ?? ((ms) => sleep(ms)),
18251
+ now: deps.now
18252
+ });
18253
+ }
18254
+
18255
+ // src/oauth/revoke.ts
18256
+ async function revokeToken(options) {
18257
+ try {
18258
+ const issuer = assertRasterOrLocalHost(options.tokens.issuer, "auth issuer");
18259
+ const metadata = await discover(issuer, options.fetch);
18260
+ const clientId = await ensureClientId({
18261
+ issuer,
18262
+ metadata,
18263
+ env: options.env,
18264
+ fetch: options.fetch,
18265
+ pinnedClientId: options.pinnedClientId
18266
+ });
18267
+ const response = await options.fetch(metadata.revocationEndpoint, {
18268
+ method: "POST",
18269
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
18270
+ body: new URLSearchParams({
18271
+ token: options.tokens.refreshToken,
18272
+ token_type_hint: "refresh_token",
18273
+ client_id: clientId
18274
+ }).toString(),
18275
+ signal: AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS)
18276
+ });
18277
+ return response.ok;
18278
+ } catch {
18279
+ return false;
18280
+ }
18281
+ }
18282
+
17564
18283
  // src/commands/auth.ts
17565
18284
  async function fetchViewer(context, apiKey) {
17566
18285
  const client = context.clientFactory({ apiKey });
17567
18286
  return unwrapResult(() => client.GET("/me", { params: { header: VERSION_HEADER } }));
17568
18287
  }
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.");
18288
+ async function loginCommand(context, options = {}) {
18289
+ if (context.apiKey?.source === "flag") {
18290
+ await apiKeyLogin(context, context.apiKey.key);
18291
+ return;
17574
18292
  }
18293
+ if (context.env.RASTER_API_KEY && !context.isInteractive && !options.device && !options.browser) {
18294
+ await apiKeyLogin(context, context.env.RASTER_API_KEY);
18295
+ return;
18296
+ }
18297
+ await oauthLogin(context, options);
18298
+ }
18299
+ async function apiKeyLogin(context, key) {
17575
18300
  const viewer = await fetchViewer(context, key);
17576
18301
  const config2 = await readConfig(context.env);
17577
18302
  await writeConfig({
17578
18303
  ...config2,
17579
18304
  apiKey: key,
17580
18305
  organizationId: viewer?.organizationId,
17581
- libraries: (viewer?.libraries ?? []).filter((library) => typeof library === "string")
18306
+ libraries: (viewer?.libraries ?? []).map((entry) => entry.library),
18307
+ oauth: undefined
17582
18308
  }, context.env);
17583
18309
  if (context.json) {
17584
18310
  printJson(context, viewer);
17585
18311
  return;
17586
18312
  }
17587
18313
  const organizationLabel = viewer?.organizationName ?? viewer?.organizationId ?? "your organization";
17588
- note(context, `Logged in to ${organizationLabel} as ${maskApiKey(key)}. Key stored in ${configPath(context.env)}.`);
18314
+ note(context, `Logged in to ${organizationLabel}.`);
18315
+ }
18316
+ function chooseLoginMethod(context, options) {
18317
+ if (options.device && options.browser) {
18318
+ throw new UsageError("Pass only one of --device or --browser.");
18319
+ }
18320
+ if (options.browser) {
18321
+ if (!context.canOpenBrowser()) {
18322
+ throw new UsageError("No browser is available to open. Use --device, or run on a machine with a display.");
18323
+ }
18324
+ return "browser";
18325
+ }
18326
+ if (options.device)
18327
+ return "device";
18328
+ return context.isInteractive && context.canOpenBrowser() ? "browser" : "device";
18329
+ }
18330
+ async function oauthLogin(context, options) {
18331
+ const method = chooseLoginMethod(context, options);
18332
+ const tokens = await runOAuthLogin(method, {
18333
+ issuer: context.issuer,
18334
+ env: context.env,
18335
+ fetch: context.fetch,
18336
+ pinnedClientId: context.pinnedClientId,
18337
+ now: context.now,
18338
+ openBrowser: context.openBrowser,
18339
+ onDevicePrompt: (prompt) => {
18340
+ const target = prompt.verificationUriComplete ? bold(prompt.verificationUriComplete, context.color) : `${bold(prompt.verificationUri, context.color)} and enter code ${bold(prompt.userCode, context.color)}`;
18341
+ note(context, "");
18342
+ note(context, `Visit ${target}`);
18343
+ note(context, "");
18344
+ note(context, dim("Waiting for authentication…", context.color));
18345
+ },
18346
+ onAuthorizeUrl: (url2) => {
18347
+ note(context, "");
18348
+ note(context, `Visit ${bold(url2, context.color)}`);
18349
+ note(context, "");
18350
+ note(context, dim("Waiting for authentication…", context.color));
18351
+ }
18352
+ });
18353
+ await writeOAuthTokens(tokens, context.env);
18354
+ if (context.json) {
18355
+ printJson(context, { issuer: tokens.issuer, scope: tokens.scope, resource: tokens.resource });
18356
+ return;
18357
+ }
18358
+ note(context, "Logged in.");
17589
18359
  }
17590
18360
  async function logoutCommand(context) {
17591
18361
  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.");
18362
+ if (config2.oauth) {
18363
+ await revokeToken({
18364
+ tokens: config2.oauth,
18365
+ env: context.env,
18366
+ fetch: context.fetch,
18367
+ pinnedClientId: context.pinnedClientId
18368
+ });
18369
+ }
18370
+ const cleared = { ...config2 };
18371
+ delete cleared.apiKey;
18372
+ delete cleared.oauth;
18373
+ await writeConfig(cleared, context.env);
18374
+ note(context, "Logged out.");
17595
18375
  }
17596
18376
  async function statusCommand(context) {
17597
- if (!context.apiKey) {
18377
+ if (!context.credential) {
17598
18378
  if (context.json) {
17599
18379
  printJson(context, { authenticated: false });
17600
18380
  return;
17601
18381
  }
17602
- note(context, "Not authenticated. Run `raster auth login` or set RASTER_API_KEY.");
18382
+ note(context, "Not authenticated. Run `raster auth login`.");
18383
+ return;
18384
+ }
18385
+ if (context.credential.kind === "oauth") {
18386
+ statusForOAuth(context, context.credential.tokens);
18387
+ return;
18388
+ }
18389
+ await statusForApiKey(context, context.credential.key, context.credential.source);
18390
+ }
18391
+ function statusForOAuth(context, tokens) {
18392
+ const fresh = isAccessTokenFresh(tokens, context.now());
18393
+ if (context.json) {
18394
+ printJson(context, { authenticated: true, kind: "oauth", issuer: tokens.issuer, scope: tokens.scope, fresh });
17603
18395
  return;
17604
18396
  }
17605
- const { key, source } = context.apiKey;
18397
+ note(context, `OAuth login on ${tokens.issuer}, ${tokens.scope} scope.`);
18398
+ note(context, fresh ? "Token fresh." : "Token refreshes on next use.");
18399
+ }
18400
+ async function statusForApiKey(context, key, source) {
17606
18401
  let viewer = null;
17607
18402
  let failureMessage = null;
17608
18403
  try {
@@ -17611,15 +18406,10 @@ async function statusCommand(context) {
17611
18406
  failureMessage = error51 instanceof Error ? error51.message : String(error51);
17612
18407
  }
17613
18408
  if (context.json) {
17614
- printJson(context, {
17615
- authenticated: true,
17616
- source,
17617
- keyPrefix: maskApiKey(key),
17618
- organization: viewer ?? null
17619
- });
18409
+ printJson(context, { authenticated: true, kind: "apiKey", source, keyPrefix: maskApiKey(key), organization: viewer ?? null });
17620
18410
  return;
17621
18411
  }
17622
- note(context, `API key ${maskApiKey(key)} (from ${source}).`);
18412
+ note(context, `API key ${maskApiKey(key)}, from ${source}.`);
17623
18413
  if (viewer) {
17624
18414
  note(context, `Organization: ${viewer.organizationName ?? viewer.organizationId ?? "unknown"} · plan: ${viewer.plan ?? "unknown"}`);
17625
18415
  return;
@@ -17627,25 +18417,34 @@ async function statusCommand(context) {
17627
18417
  note(context, `Could not validate the key against the API: ${failureMessage ?? "unknown error"}`);
17628
18418
  }
17629
18419
  async function whoamiCommand(context) {
17630
- const { key } = requireApiKey(context);
17631
- const viewer = await fetchViewer(context, key);
18420
+ requireAuth(context);
18421
+ const viewer = await unwrapResult(() => context.clientFactory().GET("/me", { params: { header: VERSION_HEADER } }));
17632
18422
  if (context.json) {
17633
18423
  printJson(context, viewer);
17634
18424
  return;
17635
18425
  }
18426
+ const isBroadOAuthLogin = context.credential?.kind === "oauth" && context.credential.tokens.scope !== "org";
18427
+ const organizations = viewer?.organizations ?? [];
18428
+ if (isBroadOAuthLogin && organizations.length > 0) {
18429
+ renderTable(context, organizations, [
18430
+ { header: "organization", value: (organization) => organization.organizationId },
18431
+ { header: "name", value: (organization) => organization.organizationName ?? "" }
18432
+ ]);
18433
+ return;
18434
+ }
17636
18435
  renderRecord(context, [
17637
18436
  { label: "organization", value: viewer?.organizationId ?? "" },
17638
18437
  { label: "name", value: viewer?.organizationName ?? "" },
17639
18438
  { label: "plan", value: viewer?.plan ?? "" },
17640
- { label: "libraries", value: (viewer?.libraries ?? []).join(", ") }
18439
+ { label: "libraries", value: (viewer?.libraries ?? []).map((entry) => entry.library).join(", ") }
17641
18440
  ]);
17642
18441
  }
17643
18442
  function registerAuthCommands(program2, runAction) {
17644
18443
  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)));
18444
+ 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 })));
18445
+ auth.command("logout").description("Revoke the OAuth token (if any) and remove stored credentials").action((_options, command) => runAction(command, (context) => logoutCommand(context)));
18446
+ auth.command("status").description("Show which credential is in use and where it came from").action((_options, command) => runAction(command, (context) => statusCommand(context)));
18447
+ program2.command("whoami").description("Show the organizations and libraries the current credential can access").action((_options, command) => runAction(command, (context) => whoamiCommand(context)));
17649
18448
  }
17650
18449
 
17651
18450
  // src/commands/libraries.ts
@@ -17661,7 +18460,7 @@ var listOptionsSchema2 = exports_external.object({
17661
18460
  pageSize: exports_external.coerce.number().int().min(1).optional()
17662
18461
  });
17663
18462
  async function listLibrariesCommand(context, options) {
17664
- requireApiKey(context);
18463
+ requireAuth(context);
17665
18464
  const organizationId = await context.resolveOrg();
17666
18465
  const client = context.clientFactory();
17667
18466
  const libraries = await unwrapResult(() => client.GET("/organizations/{organizationId}/libraries", {
@@ -17682,7 +18481,7 @@ var createOptionsSchema = exports_external.object({
17682
18481
  slug: exports_external.string().min(1).optional()
17683
18482
  });
17684
18483
  async function createLibraryCommand(context, options) {
17685
- requireApiKey(context);
18484
+ requireAuth(context);
17686
18485
  const organizationId = await context.resolveOrg();
17687
18486
  const client = context.clientFactory();
17688
18487
  const library = await unwrapResult(() => client.POST("/organizations/{organizationId}/libraries", {
@@ -17703,7 +18502,7 @@ var renameOptionsSchema = exports_external.object({
17703
18502
  name: exports_external.string().min(1)
17704
18503
  });
17705
18504
  async function renameLibraryCommand(context, options) {
17706
- requireApiKey(context);
18505
+ requireAuth(context);
17707
18506
  const organizationId = await context.resolveOrg();
17708
18507
  const libraryId = await context.resolveLibrary();
17709
18508
  const client = context.clientFactory();
@@ -17779,7 +18578,7 @@ var listOptionsSchema3 = exports_external.object({
17779
18578
  limit: exports_external.coerce.number().int().min(1).optional()
17780
18579
  });
17781
18580
  async function listTagsCommand(context, options) {
17782
- requireApiKey(context);
18581
+ requireAuth(context);
17783
18582
  const organizationId = await context.resolveOrg();
17784
18583
  const libraryId = await context.resolveLibrary();
17785
18584
  const client = context.clientFactory();
@@ -17802,7 +18601,7 @@ var mutateOptionsSchema = exports_external.object({
17802
18601
  async function tagAssetsCommand(context, assetIds, options) {
17803
18602
  if (assetIds.length === 0)
17804
18603
  throw new UsageError("Provide at least one asset id.");
17805
- requireApiKey(context);
18604
+ requireAuth(context);
17806
18605
  const organizationId = await context.resolveOrg();
17807
18606
  const libraryId = await context.resolveLibrary();
17808
18607
  const client = context.clientFactory();
@@ -17819,7 +18618,7 @@ async function tagAssetsCommand(context, assetIds, options) {
17819
18618
  async function untagAssetsCommand(context, assetIds, options) {
17820
18619
  if (assetIds.length === 0)
17821
18620
  throw new UsageError("Provide at least one asset id.");
17822
- requireApiKey(context);
18621
+ requireAuth(context);
17823
18622
  const organizationId = await context.resolveOrg();
17824
18623
  const libraryId = await context.resolveLibrary();
17825
18624
  const client = context.clientFactory();