@raster-app/cli 0.1.1 → 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 +88 -25
  2. package/dist/index.js +978 -148
  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.1.1",
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,8 +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
- apiKey: exports_external.string().min(1).optional()
17049
+ apiKey: exports_external.string().min(1).optional(),
17050
+ organizationId: exports_external.string().min(1).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()
17036
17056
  });
17037
17057
  function configDir(env) {
17038
17058
  if (env.RASTER_CONFIG_HOME)
@@ -17068,13 +17088,334 @@ async function writeConfig(config2, env) {
17068
17088
  `, { mode: CONFIG_FILE_MODE });
17069
17089
  await chmod(path2, CONFIG_FILE_MODE);
17070
17090
  }
17071
- 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) {
17072
17411
  if (options.flagKey)
17073
- 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 };
17074
17415
  if (options.env.RASTER_API_KEY)
17075
- return { key: options.env.RASTER_API_KEY, source: "env" };
17416
+ return { kind: "apiKey", key: options.env.RASTER_API_KEY, source: "env" };
17076
17417
  if (options.config.apiKey)
17077
- return { key: options.config.apiKey, source: "config" };
17418
+ return { kind: "apiKey", key: options.config.apiKey, source: "config" };
17078
17419
  return null;
17079
17420
  }
17080
17421
 
@@ -17191,10 +17532,15 @@ function promptSecret(message) {
17191
17532
  }
17192
17533
 
17193
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
+ }
17194
17540
  async function createCommandContext(globals, overrides = {}) {
17195
17541
  const env = overrides.env ?? process.env;
17196
17542
  const config2 = await readConfig(env);
17197
- const apiKey = resolveApiKey({ flagKey: globals.apiKey, env, config: config2 });
17543
+ const now = overrides.now ?? Date.now;
17198
17544
  const output = {
17199
17545
  json: globals.json === true,
17200
17546
  verbose: globals.verbose === true,
@@ -17203,22 +17549,125 @@ async function createCommandContext(globals, overrides = {}) {
17203
17549
  };
17204
17550
  const baseUrl = validateBaseUrl(env.RASTER_API_BASE_URL ?? DEFAULT_BASE_URL);
17205
17551
  const baseFetch = overrides.fetch ?? globalThis.fetch;
17206
- 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);
17562
+ const clientFactory = (clientOverrides) => createApiClient({
17563
+ baseUrl,
17564
+ apiKey: clientOverrides ? clientOverrides.apiKey : bearer,
17565
+ fetch: instrumentedFetch
17566
+ });
17567
+ let viewerPromise = null;
17568
+ const loadViewer = () => {
17569
+ if (!viewerPromise) {
17570
+ if (!credential) {
17571
+ throw new AuthSetupError("Not authenticated. Run `raster auth login` or set RASTER_API_KEY.");
17572
+ }
17573
+ viewerPromise = unwrapResult(() => clientFactory().GET("/me", { params: { header: VERSION_HEADER } }));
17574
+ }
17575
+ return viewerPromise;
17576
+ };
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
+ };
17207
17614
  return {
17208
17615
  ...output,
17209
17616
  env,
17210
17617
  baseUrl,
17211
17618
  isInteractive: overrides.isInteractive ?? process.stdin.isTTY === true,
17619
+ credential,
17212
17620
  apiKey,
17621
+ bearerToken: bearer,
17213
17622
  fetch: instrumentedFetch,
17214
- clientFactory: (clientOverrides) => createApiClient({
17215
- baseUrl,
17216
- apiKey: clientOverrides ? clientOverrides.apiKey : apiKey?.key ?? null,
17217
- fetch: instrumentedFetch
17218
- }),
17623
+ clientFactory,
17219
17624
  confirm: overrides.confirm ?? promptConfirm,
17220
- promptSecret: overrides.promptSecret ?? promptSecret
17221
- };
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,
17631
+ explicitLibrary: globals.library,
17632
+ resolveOrg: resolveOrgId,
17633
+ resolveLibrary: async () => {
17634
+ if (globals.library)
17635
+ return globals.library;
17636
+ const libraries = toLibraryIds(await librariesForTarget());
17637
+ const onlyLibrary = libraries[0];
17638
+ if (libraries.length === 1 && onlyLibrary)
17639
+ return onlyLibrary;
17640
+ if (libraries.length === 0) {
17641
+ throw new AuthSetupError("No library access for this login. Pass --library <id>.");
17642
+ }
17643
+ throw new UsageError(`This login can access ${libraries.length} libraries (${libraries.join(", ")}). Pass --library <id>.`);
17644
+ }
17645
+ };
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 };
17222
17671
  }
17223
17672
  function instrumentFetch(baseFetch, output, maskedKey) {
17224
17673
  return async (input, init) => {
@@ -17236,10 +17685,15 @@ function instrumentFetch(baseFetch, output, maskedKey) {
17236
17685
  }
17237
17686
  };
17238
17687
  }
17239
- function requireApiKey(context) {
17240
- if (context.apiKey)
17241
- return context.apiKey;
17242
- 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.");
17243
17697
  }
17244
17698
  async function executeCommand(context, action) {
17245
17699
  try {
@@ -17284,11 +17738,7 @@ var SEARCH_HIT_COLUMNS = [
17284
17738
  { header: "NAME", value: (hit) => hit.name ?? "" },
17285
17739
  { header: "LIBRARY", value: (hit) => hit.libraryId ?? "" }
17286
17740
  ];
17287
- var inLibraryOptionsSchema = exports_external.object({
17288
- org: exports_external.string().min(1),
17289
- library: exports_external.string().min(1)
17290
- });
17291
- var listOptionsSchema = inLibraryOptionsSchema.extend({
17741
+ var listOptionsSchema = exports_external.object({
17292
17742
  page: exports_external.coerce.number().int().min(1).optional(),
17293
17743
  pageSize: exports_external.coerce.number().int().min(1).optional(),
17294
17744
  tag: exports_external.array(exports_external.string().min(1)).default([])
@@ -17297,11 +17747,13 @@ async function listAssetsCommand(context, options) {
17297
17747
  if (options.tag.length > MAX_TAGS_PER_QUERY) {
17298
17748
  throw new ValidationError(`At most ${MAX_TAGS_PER_QUERY} --tag filters are supported per query.`);
17299
17749
  }
17300
- requireApiKey(context);
17750
+ requireAuth(context);
17751
+ const organizationId = await context.resolveOrg();
17752
+ const libraryId = await context.resolveLibrary();
17301
17753
  const client = context.clientFactory();
17302
17754
  const assets = await unwrapResult(() => client.GET("/organizations/{organizationId}/libraries/{libraryId}/assets", {
17303
17755
  params: {
17304
- path: { organizationId: options.org, libraryId: options.library },
17756
+ path: { organizationId, libraryId },
17305
17757
  header: VERSION_HEADER,
17306
17758
  query: {
17307
17759
  page: options.page,
@@ -17316,10 +17768,9 @@ async function listAssetsCommand(context, options) {
17316
17768
  }
17317
17769
  renderTable(context, assets, ASSET_COLUMNS);
17318
17770
  }
17319
- var getOptionsSchema = inLibraryOptionsSchema;
17320
- async function getAssetCommand(context, assetId, options) {
17321
- requireApiKey(context);
17322
- const asset = await fetchAsset(context, options.org, options.library, assetId);
17771
+ async function getAssetCommand(context, assetId) {
17772
+ requireAuth(context);
17773
+ const asset = await fetchAsset(context, assetId);
17323
17774
  if (context.json) {
17324
17775
  printJson(context, asset);
17325
17776
  return;
@@ -17335,28 +17786,29 @@ async function getAssetCommand(context, assetId, options) {
17335
17786
  { label: "url", value: asset.url ?? "" }
17336
17787
  ]);
17337
17788
  }
17338
- async function fetchAsset(context, organizationId, libraryId, assetId) {
17789
+ async function fetchAsset(context, assetId) {
17790
+ const organizationId = await context.resolveOrg();
17791
+ const libraryId = await context.resolveLibrary();
17339
17792
  const client = context.clientFactory();
17340
17793
  return unwrapResult(() => client.GET("/organizations/{organizationId}/libraries/{libraryId}/assets/{assetId}", {
17341
17794
  params: { path: { organizationId, libraryId, assetId }, header: VERSION_HEADER }
17342
17795
  }));
17343
17796
  }
17344
17797
  var searchOptionsSchema = exports_external.object({
17345
- org: exports_external.string().min(1),
17346
- library: exports_external.array(exports_external.string().min(1)).default([]),
17347
17798
  page: exports_external.coerce.number().int().min(1).optional(),
17348
17799
  pageSize: exports_external.coerce.number().int().min(1).optional()
17349
17800
  });
17350
17801
  async function searchAssetsCommand(context, query, options) {
17351
- requireApiKey(context);
17802
+ requireAuth(context);
17803
+ const organizationId = await context.resolveOrg();
17352
17804
  const client = context.clientFactory();
17353
17805
  const result = await unwrapResult(() => client.GET("/organizations/{organizationId}/search/assets", {
17354
17806
  params: {
17355
- path: { organizationId: options.org },
17807
+ path: { organizationId },
17356
17808
  header: VERSION_HEADER,
17357
17809
  query: {
17358
17810
  q: query,
17359
- libraries: options.library.length > 0 ? options.library.join(",") : undefined,
17811
+ libraries: context.explicitLibrary,
17360
17812
  page: options.page,
17361
17813
  pageSize: options.pageSize
17362
17814
  }
@@ -17369,13 +17821,13 @@ async function searchAssetsCommand(context, query, options) {
17369
17821
  note(context, `${result.found} result(s), page ${result.page}.`);
17370
17822
  renderTable(context, result.hits, SEARCH_HIT_COLUMNS);
17371
17823
  }
17372
- var downloadOptionsSchema = inLibraryOptionsSchema.extend({
17824
+ var downloadOptionsSchema = exports_external.object({
17373
17825
  output: exports_external.string().min(1).optional(),
17374
17826
  force: exports_external.boolean().default(false)
17375
17827
  });
17376
17828
  async function downloadAssetCommand(context, assetId, options) {
17377
- requireApiKey(context);
17378
- const asset = await fetchAsset(context, options.org, options.library, assetId);
17829
+ requireAuth(context);
17830
+ const asset = await fetchAsset(context, assetId);
17379
17831
  const fileUrl = asset.url;
17380
17832
  if (!fileUrl)
17381
17833
  throw new CliError("Asset has no file URL to download.", EXIT_CODES.generic);
@@ -17399,11 +17851,12 @@ async function downloadAssetCommand(context, assetId, options) {
17399
17851
  var uploadedAssetsSchema = exports_external.object({
17400
17852
  assets: exports_external.array(exports_external.looseObject({ id: exports_external.string().optional(), name: exports_external.string().nullish() }))
17401
17853
  });
17402
- var uploadOptionsSchema = inLibraryOptionsSchema;
17403
- async function uploadAssetsCommand(context, files, options) {
17854
+ async function uploadAssetsCommand(context, files) {
17404
17855
  if (files.length === 0)
17405
17856
  throw new UsageError("Provide at least one file to upload.");
17406
- const { key } = requireApiKey(context);
17857
+ const bearer = requireBearer(context);
17858
+ const organizationId = await context.resolveOrg();
17859
+ const libraryId = await context.resolveLibrary();
17407
17860
  const batches = chunk(files, MAX_UPLOAD_FILES);
17408
17861
  const uploadedNames = [];
17409
17862
  const payloads = [];
@@ -17421,7 +17874,7 @@ async function uploadAssetsCommand(context, files, options) {
17421
17874
  if (batches.length > 1) {
17422
17875
  note(context, `Uploading batch ${batchIndex + 1}/${batches.length} (${batch.length} files)…`);
17423
17876
  }
17424
- const response = await context.fetch(`${context.baseUrl}/organizations/${options.org}/libraries/${options.library}/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 });
17425
17878
  const payload = await parseEnvelope(response);
17426
17879
  payloads.push(payload);
17427
17880
  const parsed = uploadedAssetsSchema.safeParse(payload);
@@ -17439,7 +17892,7 @@ async function uploadAssetsCommand(context, files, options) {
17439
17892
  `);
17440
17893
  }
17441
17894
  }
17442
- var removeOptionsSchema = inLibraryOptionsSchema.extend({
17895
+ var removeOptionsSchema = exports_external.object({
17443
17896
  yes: exports_external.boolean().default(false)
17444
17897
  });
17445
17898
  async function removeAssetsCommand(context, assetIds, options) {
@@ -17448,7 +17901,9 @@ async function removeAssetsCommand(context, assetIds, options) {
17448
17901
  if (assetIds.length > MAX_DELETE_IDS) {
17449
17902
  throw new ValidationError(`At most ${MAX_DELETE_IDS} asset ids are supported per call.`);
17450
17903
  }
17451
- requireApiKey(context);
17904
+ requireAuth(context);
17905
+ const organizationId = await context.resolveOrg();
17906
+ const libraryId = await context.resolveLibrary();
17452
17907
  if (!options.yes && context.isInteractive) {
17453
17908
  const confirmed = await context.confirm(`Move ${assetIds.length} asset(s) to trash?`);
17454
17909
  if (!confirmed) {
@@ -17458,10 +17913,7 @@ async function removeAssetsCommand(context, assetIds, options) {
17458
17913
  }
17459
17914
  const client = context.clientFactory();
17460
17915
  const result = await unwrapResult(() => client.DELETE("/organizations/{organizationId}/libraries/{libraryId}/assets", {
17461
- params: {
17462
- path: { organizationId: options.org, libraryId: options.library },
17463
- header: VERSION_HEADER
17464
- },
17916
+ params: { path: { organizationId, libraryId }, header: VERSION_HEADER },
17465
17917
  body: { ids: assetIds }
17466
17918
  }));
17467
17919
  if (context.json) {
@@ -17470,15 +17922,17 @@ async function removeAssetsCommand(context, assetIds, options) {
17470
17922
  }
17471
17923
  note(context, `Moved ${assetIds.length} asset(s) to trash. They are recoverable from the library trash.`);
17472
17924
  }
17473
- var describeOptionsSchema = inLibraryOptionsSchema.extend({
17925
+ var describeOptionsSchema = exports_external.object({
17474
17926
  text: exports_external.string().min(1)
17475
17927
  });
17476
17928
  async function describeAssetCommand(context, assetId, options) {
17477
- requireApiKey(context);
17929
+ requireAuth(context);
17930
+ const organizationId = await context.resolveOrg();
17931
+ const libraryId = await context.resolveLibrary();
17478
17932
  const client = context.clientFactory();
17479
17933
  const asset = await unwrapResult(() => client.PATCH("/organizations/{organizationId}/libraries/{libraryId}/assets/{assetId}/description", {
17480
17934
  params: {
17481
- path: { organizationId: options.org, libraryId: options.library, assetId },
17935
+ path: { organizationId, libraryId, assetId },
17482
17936
  header: VERSION_HEADER
17483
17937
  },
17484
17938
  body: { description: options.text }
@@ -17489,36 +17943,327 @@ async function describeAssetCommand(context, assetId, options) {
17489
17943
  }
17490
17944
  note(context, "Description updated.");
17491
17945
  }
17492
- var transferOptionsSchema = inLibraryOptionsSchema.extend({
17493
- to: exports_external.string().min(1),
17494
- asset: exports_external.array(exports_external.string().min(1)).min(1)
17946
+ var transferOptionsSchema = exports_external.object({
17947
+ to: exports_external.string().min(1)
17495
17948
  });
17496
- async function transferAssetsCommand(context, options) {
17497
- requireApiKey(context);
17949
+ async function transferAssetsCommand(context, assetIds, options) {
17950
+ if (assetIds.length === 0)
17951
+ throw new UsageError("Provide at least one asset id.");
17952
+ requireAuth(context);
17953
+ const organizationId = await context.resolveOrg();
17954
+ const libraryId = await context.resolveLibrary();
17498
17955
  const client = context.clientFactory();
17499
17956
  const result = await unwrapResult(() => client.POST("/organizations/{organizationId}/libraries/{libraryId}/assets/transfer", {
17500
- params: {
17501
- path: { organizationId: options.org, libraryId: options.library },
17502
- header: VERSION_HEADER
17503
- },
17504
- body: { targetLibraryId: options.to, assetIds: options.asset }
17957
+ params: { path: { organizationId, libraryId }, header: VERSION_HEADER },
17958
+ body: { targetLibraryId: options.to, assetIds }
17505
17959
  }));
17506
17960
  if (context.json) {
17507
17961
  printJson(context, result);
17508
17962
  return;
17509
17963
  }
17510
- note(context, `Transferred ${options.asset.length} asset(s) to library ${options.to}.`);
17964
+ note(context, `Transferred ${assetIds.length} asset(s) to library ${options.to}.`);
17511
17965
  }
17512
17966
  function registerAssetCommands(program2, runAction) {
17513
17967
  const assets = program2.command("assets").description("Work with assets in a library");
17514
- assets.command("ls").description("List assets in a library").requiredOption("--org <organizationId>", "Organization id").requiredOption("--library <libraryId>", "Library id").option("--page <number>", "Page number (1-based)").option("--page-size <number>", "Results per page").option("--tag <tag...>", "Filter by tag (repeatable, up to 5)").action((options, command) => runAction(command, (context) => listAssetsCommand(context, parseOptions(listOptionsSchema, options))));
17515
- assets.command("get").description("Show a single asset").argument("<assetId>", "Asset id").requiredOption("--org <organizationId>", "Organization id").requiredOption("--library <libraryId>", "Library id").action((assetId, options, command) => runAction(command, (context) => getAssetCommand(context, assetId, parseOptions(getOptionsSchema, options))));
17516
- assets.command("search").description("Search assets across the organization").argument("<query>", "Search query").requiredOption("--org <organizationId>", "Organization id").option("--library <libraryId...>", "Restrict to specific libraries (repeatable)").option("--page <number>", "Page number (1-based)").option("--page-size <number>", "Results per page").action((query, options, command) => runAction(command, (context) => searchAssetsCommand(context, query, parseOptions(searchOptionsSchema, options))));
17517
- assets.command("download").description("Download an asset's file").argument("<assetId>", "Asset id").requiredOption("--org <organizationId>", "Organization id").requiredOption("--library <libraryId>", "Library id").option("-o, --output <path>", "Target file path").option("--force", "Overwrite the target file if it exists").action((assetId, options, command) => runAction(command, (context) => downloadAssetCommand(context, assetId, parseOptions(downloadOptionsSchema, options))));
17518
- assets.command("upload").description("Upload local files as assets").argument("<files...>", "Local file paths").requiredOption("--org <organizationId>", "Organization id").requiredOption("--library <libraryId>", "Library id").action((files, options, command) => runAction(command, (context) => uploadAssetsCommand(context, files, parseOptions(uploadOptionsSchema, options))));
17519
- assets.command("rm").description("Move assets to trash (recoverable)").argument("<assetIds...>", "Asset ids").requiredOption("--org <organizationId>", "Organization id").requiredOption("--library <libraryId>", "Library id").option("--yes", "Skip the confirmation prompt").action((assetIds, options, command) => runAction(command, (context) => removeAssetsCommand(context, assetIds, parseOptions(removeOptionsSchema, options))));
17520
- assets.command("describe").description("Set an asset's description").argument("<assetId>", "Asset id").requiredOption("--org <organizationId>", "Organization id").requiredOption("--library <libraryId>", "Library id").requiredOption("--text <description>", "Description text").action((assetId, options, command) => runAction(command, (context) => describeAssetCommand(context, assetId, parseOptions(describeOptionsSchema, options))));
17521
- assets.command("transfer").description("Move assets to another library").requiredOption("--org <organizationId>", "Organization id").requiredOption("--library <libraryId>", "Source library id").requiredOption("--to <libraryId>", "Target library id").requiredOption("--asset <assetId...>", "Asset ids (repeatable)").action((options, command) => runAction(command, (context) => transferAssetsCommand(context, parseOptions(transferOptionsSchema, options))));
17968
+ assets.command("ls").description("List assets in the library").option("--page <number>", "Page number (1-based)").option("--page-size <number>", "Results per page").option("--tag <tag...>", "Filter by tag (repeatable, up to 5)").action((options, command) => runAction(command, (context) => listAssetsCommand(context, parseOptions(listOptionsSchema, options))));
17969
+ assets.command("get").description("Show a single asset").argument("<assetId>", "Asset id").action((assetId, _options, command) => runAction(command, (context) => getAssetCommand(context, assetId)));
17970
+ assets.command("search").description("Search assets across the organization (pass --library to scope to one)").argument("<query>", "Search query").option("--page <number>", "Page number (1-based)").option("--page-size <number>", "Results per page").action((query, options, command) => runAction(command, (context) => searchAssetsCommand(context, query, parseOptions(searchOptionsSchema, options))));
17971
+ assets.command("download").description("Download an asset's file").argument("<assetId>", "Asset id").option("-o, --output <path>", "Target file path").option("--force", "Overwrite the target file if it exists").action((assetId, options, command) => runAction(command, (context) => downloadAssetCommand(context, assetId, parseOptions(downloadOptionsSchema, options))));
17972
+ assets.command("upload").description("Upload local files as assets").argument("<files...>", "Local file paths").action((files, _options, command) => runAction(command, (context) => uploadAssetsCommand(context, files)));
17973
+ assets.command("rm").description("Move assets to trash (recoverable)").argument("<assetIds...>", "Asset ids").option("--yes", "Skip the confirmation prompt").action((assetIds, options, command) => runAction(command, (context) => removeAssetsCommand(context, assetIds, parseOptions(removeOptionsSchema, options))));
17974
+ assets.command("describe").description("Set an asset's description").argument("<assetId>", "Asset id").requiredOption("--text <description>", "Description text").action((assetId, options, command) => runAction(command, (context) => describeAssetCommand(context, assetId, parseOptions(describeOptionsSchema, options))));
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))));
17976
+ }
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
+ }
17522
18267
  }
17523
18268
 
17524
18269
  // src/commands/auth.ts
@@ -17526,32 +18271,97 @@ async function fetchViewer(context, apiKey) {
17526
18271
  const client = context.clientFactory({ apiKey });
17527
18272
  return unwrapResult(() => client.GET("/me", { params: { header: VERSION_HEADER } }));
17528
18273
  }
17529
- var loginOptionsSchema = exports_external.object({
17530
- apiKey: exports_external.string().min(1).optional()
17531
- });
17532
- async function loginCommand(context, options) {
17533
- const promptedKey = options.apiKey ?? (context.isInteractive ? await context.promptSecret("Paste your Raster API key") : null);
17534
- if (!promptedKey) {
17535
- 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;
17536
18289
  }
17537
- const viewer = await fetchViewer(context, promptedKey);
18290
+ await oauthLogin(context, options);
18291
+ }
18292
+ async function apiKeyLogin(context, key) {
18293
+ const viewer = await fetchViewer(context, key);
17538
18294
  const config2 = await readConfig(context.env);
17539
- await writeConfig({ ...config2, apiKey: promptedKey }, context.env);
18295
+ await writeConfig({
18296
+ ...config2,
18297
+ apiKey: key,
18298
+ organizationId: viewer?.organizationId,
18299
+ libraries: (viewer?.libraries ?? []).map((entry) => entry.library),
18300
+ oauth: undefined
18301
+ }, context.env);
17540
18302
  if (context.json) {
17541
18303
  printJson(context, viewer);
17542
18304
  return;
17543
18305
  }
17544
18306
  const organizationLabel = viewer?.organizationName ?? viewer?.organizationId ?? "your organization";
17545
- note(context, `Logged in to ${organizationLabel} as ${maskApiKey(promptedKey)}. Key stored in ${configPath(context.env)}.`);
18307
+ note(context, `Logged in to ${organizationLabel} as ${maskApiKey(key)}. Key stored in ${configPath(context.env)}.`);
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.");
17546
18346
  }
17547
18347
  async function logoutCommand(context) {
17548
18348
  const config2 = await readConfig(context.env);
17549
- delete config2.apiKey;
17550
- await writeConfig(config2, context.env);
17551
- 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.");
17552
18362
  }
17553
18363
  async function statusCommand(context) {
17554
- if (!context.apiKey) {
18364
+ if (!context.credential) {
17555
18365
  if (context.json) {
17556
18366
  printJson(context, { authenticated: false });
17557
18367
  return;
@@ -17559,7 +18369,22 @@ async function statusCommand(context) {
17559
18369
  note(context, "Not authenticated. Run `raster auth login` or set RASTER_API_KEY.");
17560
18370
  return;
17561
18371
  }
17562
- 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) {
17563
18388
  let viewer = null;
17564
18389
  let failureMessage = null;
17565
18390
  try {
@@ -17568,12 +18393,7 @@ async function statusCommand(context) {
17568
18393
  failureMessage = error51 instanceof Error ? error51.message : String(error51);
17569
18394
  }
17570
18395
  if (context.json) {
17571
- printJson(context, {
17572
- authenticated: true,
17573
- source,
17574
- keyPrefix: maskApiKey(key),
17575
- organization: viewer ?? null
17576
- });
18396
+ printJson(context, { authenticated: true, kind: "apiKey", source, keyPrefix: maskApiKey(key), organization: viewer ?? null });
17577
18397
  return;
17578
18398
  }
17579
18399
  note(context, `API key ${maskApiKey(key)} (from ${source}).`);
@@ -17584,25 +18404,34 @@ async function statusCommand(context) {
17584
18404
  note(context, `Could not validate the key against the API: ${failureMessage ?? "unknown error"}`);
17585
18405
  }
17586
18406
  async function whoamiCommand(context) {
17587
- const { key } = requireApiKey(context);
17588
- const viewer = await fetchViewer(context, key);
18407
+ requireAuth(context);
18408
+ const viewer = await unwrapResult(() => context.clientFactory().GET("/me", { params: { header: VERSION_HEADER } }));
17589
18409
  if (context.json) {
17590
18410
  printJson(context, viewer);
17591
18411
  return;
17592
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
+ }
17593
18422
  renderRecord(context, [
17594
18423
  { label: "organization", value: viewer?.organizationId ?? "" },
17595
18424
  { label: "name", value: viewer?.organizationName ?? "" },
17596
18425
  { label: "plan", value: viewer?.plan ?? "" },
17597
- { label: "libraries", value: (viewer?.libraries ?? []).join(", ") }
18426
+ { label: "libraries", value: (viewer?.libraries ?? []).map((entry) => entry.library).join(", ") }
17598
18427
  ]);
17599
18428
  }
17600
18429
  function registerAuthCommands(program2, runAction) {
17601
18430
  const auth = program2.command("auth").description("Manage CLI authentication");
17602
- auth.command("login").description("Validate an API key against the API and store it").option("--api-key <key>", "API key to store (prompts when omitted)").action((options, command) => runAction(command, (context) => loginCommand(context, parseOptions(loginOptionsSchema, options))));
17603
- auth.command("logout").description("Remove the stored API key").action((_options, command) => runAction(command, (context) => logoutCommand(context)));
17604
- auth.command("status").description("Show which API key is in use and where it came from").action((_options, command) => runAction(command, (context) => statusCommand(context)));
17605
- 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)));
17606
18435
  }
17607
18436
 
17608
18437
  // src/commands/libraries.ts
@@ -17614,16 +18443,16 @@ var LIBRARY_COLUMNS = [
17614
18443
  { header: "TAGS", value: (library) => String((library.tags ?? []).length) }
17615
18444
  ];
17616
18445
  var listOptionsSchema2 = exports_external.object({
17617
- org: exports_external.string().min(1),
17618
18446
  page: exports_external.coerce.number().int().min(1).optional(),
17619
18447
  pageSize: exports_external.coerce.number().int().min(1).optional()
17620
18448
  });
17621
18449
  async function listLibrariesCommand(context, options) {
17622
- requireApiKey(context);
18450
+ requireAuth(context);
18451
+ const organizationId = await context.resolveOrg();
17623
18452
  const client = context.clientFactory();
17624
18453
  const libraries = await unwrapResult(() => client.GET("/organizations/{organizationId}/libraries", {
17625
18454
  params: {
17626
- path: { organizationId: options.org },
18455
+ path: { organizationId },
17627
18456
  header: VERSION_HEADER,
17628
18457
  query: { page: options.page, pageSize: options.pageSize }
17629
18458
  }
@@ -17635,15 +18464,15 @@ async function listLibrariesCommand(context, options) {
17635
18464
  renderTable(context, libraries, LIBRARY_COLUMNS);
17636
18465
  }
17637
18466
  var createOptionsSchema = exports_external.object({
17638
- org: exports_external.string().min(1),
17639
18467
  name: exports_external.string().min(1),
17640
18468
  slug: exports_external.string().min(1).optional()
17641
18469
  });
17642
18470
  async function createLibraryCommand(context, options) {
17643
- requireApiKey(context);
18471
+ requireAuth(context);
18472
+ const organizationId = await context.resolveOrg();
17644
18473
  const client = context.clientFactory();
17645
18474
  const library = await unwrapResult(() => client.POST("/organizations/{organizationId}/libraries", {
17646
- params: { path: { organizationId: options.org }, header: VERSION_HEADER },
18475
+ params: { path: { organizationId }, header: VERSION_HEADER },
17647
18476
  body: { name: options.name, slug: options.slug }
17648
18477
  }));
17649
18478
  if (context.json) {
@@ -17657,16 +18486,16 @@ async function createLibraryCommand(context, options) {
17657
18486
  note(context, "Library created.");
17658
18487
  }
17659
18488
  var renameOptionsSchema = exports_external.object({
17660
- org: exports_external.string().min(1),
17661
- library: exports_external.string().min(1),
17662
18489
  name: exports_external.string().min(1)
17663
18490
  });
17664
18491
  async function renameLibraryCommand(context, options) {
17665
- requireApiKey(context);
18492
+ requireAuth(context);
18493
+ const organizationId = await context.resolveOrg();
18494
+ const libraryId = await context.resolveLibrary();
17666
18495
  const client = context.clientFactory();
17667
18496
  const library = await unwrapResult(() => client.PATCH("/organizations/{organizationId}/libraries/{libraryId}", {
17668
18497
  params: {
17669
- path: { organizationId: options.org, libraryId: options.library },
18498
+ path: { organizationId, libraryId },
17670
18499
  header: VERSION_HEADER
17671
18500
  },
17672
18501
  body: { name: options.name }
@@ -17679,9 +18508,9 @@ async function renameLibraryCommand(context, options) {
17679
18508
  }
17680
18509
  function registerLibraryCommands(program2, runAction) {
17681
18510
  const libraries = program2.command("libraries").description("Work with libraries");
17682
- libraries.command("ls").description("List libraries in an organization").requiredOption("--org <organizationId>", "Organization id").option("--page <number>", "Page number (1-based)").option("--page-size <number>", "Results per page").action((options, command) => runAction(command, (context) => listLibrariesCommand(context, parseOptions(listOptionsSchema2, options))));
17683
- libraries.command("create").description("Create a library").requiredOption("--org <organizationId>", "Organization id").requiredOption("--name <name>", "Library name").option("--slug <slug>", "URL slug for the library").action((options, command) => runAction(command, (context) => createLibraryCommand(context, parseOptions(createOptionsSchema, options))));
17684
- libraries.command("rename").description("Rename a library").requiredOption("--org <organizationId>", "Organization id").requiredOption("--library <libraryId>", "Library id").requiredOption("--name <name>", "New library name").action((options, command) => runAction(command, (context) => renameLibraryCommand(context, parseOptions(renameOptionsSchema, options))));
18511
+ libraries.command("ls").description("List libraries in the organization").option("--page <number>", "Page number (1-based)").option("--page-size <number>", "Results per page").action((options, command) => runAction(command, (context) => listLibrariesCommand(context, parseOptions(listOptionsSchema2, options))));
18512
+ libraries.command("create").description("Create a library").requiredOption("--name <name>", "Library name").option("--slug <slug>", "URL slug for the library").action((options, command) => runAction(command, (context) => createLibraryCommand(context, parseOptions(createOptionsSchema, options))));
18513
+ libraries.command("rename").description("Rename the library (the one the key scopes to, or pass --library)").requiredOption("--name <name>", "New library name").action((options, command) => runAction(command, (context) => renameLibraryCommand(context, parseOptions(renameOptionsSchema, options))));
17685
18514
  }
17686
18515
 
17687
18516
  // src/commands/orgs.ts
@@ -17733,16 +18562,16 @@ var TAG_COLUMNS = [
17733
18562
  { header: "TYPE", value: (tag) => tag.type ?? "" }
17734
18563
  ];
17735
18564
  var listOptionsSchema3 = exports_external.object({
17736
- org: exports_external.string().min(1),
17737
- library: exports_external.string().min(1),
17738
18565
  limit: exports_external.coerce.number().int().min(1).optional()
17739
18566
  });
17740
18567
  async function listTagsCommand(context, options) {
17741
- requireApiKey(context);
18568
+ requireAuth(context);
18569
+ const organizationId = await context.resolveOrg();
18570
+ const libraryId = await context.resolveLibrary();
17742
18571
  const client = context.clientFactory();
17743
18572
  const tags = await unwrapResult(() => client.GET("/organizations/{organizationId}/libraries/{libraryId}/tags", {
17744
18573
  params: {
17745
- path: { organizationId: options.org, libraryId: options.library },
18574
+ path: { organizationId, libraryId },
17746
18575
  header: VERSION_HEADER,
17747
18576
  query: { limit: options.limit }
17748
18577
  }
@@ -17754,53 +18583,54 @@ async function listTagsCommand(context, options) {
17754
18583
  renderTable(context, tags, TAG_COLUMNS);
17755
18584
  }
17756
18585
  var mutateOptionsSchema = exports_external.object({
17757
- org: exports_external.string().min(1),
17758
- library: exports_external.string().min(1),
17759
- asset: exports_external.array(exports_external.string().min(1)).min(1),
17760
18586
  tag: exports_external.array(exports_external.string().min(1)).min(1)
17761
18587
  });
17762
- async function tagAssetsCommand(context, options) {
17763
- requireApiKey(context);
18588
+ async function tagAssetsCommand(context, assetIds, options) {
18589
+ if (assetIds.length === 0)
18590
+ throw new UsageError("Provide at least one asset id.");
18591
+ requireAuth(context);
18592
+ const organizationId = await context.resolveOrg();
18593
+ const libraryId = await context.resolveLibrary();
17764
18594
  const client = context.clientFactory();
17765
18595
  const result = await unwrapResult(() => client.POST("/organizations/{organizationId}/libraries/{libraryId}/assets/tag", {
17766
- params: {
17767
- path: { organizationId: options.org, libraryId: options.library },
17768
- header: VERSION_HEADER
17769
- },
17770
- body: { assetIds: options.asset, tags: options.tag }
18596
+ params: { path: { organizationId, libraryId }, header: VERSION_HEADER },
18597
+ body: { assetIds, tags: options.tag }
17771
18598
  }));
17772
18599
  if (context.json) {
17773
18600
  printJson(context, result);
17774
18601
  return;
17775
18602
  }
17776
- note(context, `Tagged ${options.asset.length} asset(s) with ${options.tag.join(", ")}.`);
18603
+ note(context, `Tagged ${assetIds.length} asset(s) with ${options.tag.join(", ")}.`);
17777
18604
  }
17778
- async function untagAssetsCommand(context, options) {
17779
- requireApiKey(context);
18605
+ async function untagAssetsCommand(context, assetIds, options) {
18606
+ if (assetIds.length === 0)
18607
+ throw new UsageError("Provide at least one asset id.");
18608
+ requireAuth(context);
18609
+ const organizationId = await context.resolveOrg();
18610
+ const libraryId = await context.resolveLibrary();
17780
18611
  const client = context.clientFactory();
17781
18612
  const result = await unwrapResult(() => client.POST("/organizations/{organizationId}/libraries/{libraryId}/assets/untag", {
17782
- params: {
17783
- path: { organizationId: options.org, libraryId: options.library },
17784
- header: VERSION_HEADER
17785
- },
17786
- body: { assetIds: options.asset, tags: options.tag }
18613
+ params: { path: { organizationId, libraryId }, header: VERSION_HEADER },
18614
+ body: { assetIds, tags: options.tag }
17787
18615
  }));
17788
18616
  if (context.json) {
17789
18617
  printJson(context, result);
17790
18618
  return;
17791
18619
  }
17792
- note(context, `Removed ${options.tag.join(", ")} from ${options.asset.length} asset(s).`);
18620
+ note(context, `Removed ${options.tag.join(", ")} from ${assetIds.length} asset(s).`);
17793
18621
  }
17794
18622
  function registerTagCommands(program2, runAction) {
17795
18623
  const tags = program2.command("tags").description("Work with tags in a library");
17796
- tags.command("ls").description("List tags in a library").requiredOption("--org <organizationId>", "Organization id").requiredOption("--library <libraryId>", "Library id").option("--limit <number>", "Maximum number of tags to return").action((options, command) => runAction(command, (context) => listTagsCommand(context, parseOptions(listOptionsSchema3, options))));
17797
- tags.command("add").description("Add tags to assets").requiredOption("--org <organizationId>", "Organization id").requiredOption("--library <libraryId>", "Library id").requiredOption("--asset <assetId...>", "Asset ids (repeatable)").requiredOption("--tag <tag...>", "Tags to add (repeatable)").action((options, command) => runAction(command, (context) => tagAssetsCommand(context, parseOptions(mutateOptionsSchema, options))));
17798
- tags.command("rm").description("Remove tags from assets").requiredOption("--org <organizationId>", "Organization id").requiredOption("--library <libraryId>", "Library id").requiredOption("--asset <assetId...>", "Asset ids (repeatable)").requiredOption("--tag <tag...>", "Tags to remove (repeatable)").action((options, command) => runAction(command, (context) => untagAssetsCommand(context, parseOptions(mutateOptionsSchema, options))));
18624
+ tags.command("ls").description("List tags in the library").option("--limit <number>", "Maximum number of tags to return").action((options, command) => runAction(command, (context) => listTagsCommand(context, parseOptions(listOptionsSchema3, options))));
18625
+ tags.command("add").description("Add tags to assets").argument("<assetIds...>", "Asset ids to tag").requiredOption("--tag <tag...>", "Tags to add (repeatable)").action((assetIds, options, command) => runAction(command, (context) => tagAssetsCommand(context, assetIds, parseOptions(mutateOptionsSchema, options))));
18626
+ tags.command("rm").description("Remove tags from assets").argument("<assetIds...>", "Asset ids to untag").requiredOption("--tag <tag...>", "Tags to remove (repeatable)").action((assetIds, options, command) => runAction(command, (context) => untagAssetsCommand(context, assetIds, parseOptions(mutateOptionsSchema, options))));
17799
18627
  }
17800
18628
 
17801
18629
  // src/index.ts
17802
18630
  var globalOptionsSchema = exports_external.object({
17803
18631
  apiKey: exports_external.string().optional(),
18632
+ org: exports_external.string().optional(),
18633
+ library: exports_external.string().optional(),
17804
18634
  json: exports_external.boolean().optional(),
17805
18635
  verbose: exports_external.boolean().optional()
17806
18636
  });
@@ -17830,7 +18660,7 @@ function exitCodeForCommanderError(error51) {
17830
18660
  return EXIT_CODES.generic;
17831
18661
  }
17832
18662
  var program2 = new Command;
17833
- program2.name("raster").description("Command-line client for the Raster API").version(`${package_default.version} (Api-Version ${API_VERSION})`, "-V, --version", "Print the CLI version and the pinned Api-Version").option("--api-key <key>", "API key (overrides RASTER_API_KEY and the config file)").option("--json", "Print machine-readable JSON to stdout").option("--verbose", "Log requests to stderr (keys masked)").exitOverride();
18663
+ program2.name("raster").description("Command-line client for the Raster API").version(`${package_default.version} (Api-Version ${API_VERSION})`, "-V, --version", "Print the CLI version and the pinned Api-Version").option("--api-key <key>", "API key (overrides RASTER_API_KEY and the config file)").option("--org <organizationId>", "Organization id (derived from the API key when omitted)").option("--library <libraryId>", "Library id (derived from the API key when it has one library)").option("--json", "Print machine-readable JSON to stdout").option("--verbose", "Log requests to stderr (keys masked)").exitOverride();
17834
18664
  registerAuthCommands(program2, runAction);
17835
18665
  registerLibraryCommands(program2, runAction);
17836
18666
  registerAssetCommands(program2, runAction);