@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.
- package/README.md +88 -25
- package/dist/index.js +978 -148
- 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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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:"
|
|
16902
|
-
throw new UsageError(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
17240
|
-
if (context.
|
|
17241
|
-
return context.
|
|
17242
|
-
throw new AuthSetupError("
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
17320
|
-
|
|
17321
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
17807
|
+
path: { organizationId },
|
|
17356
17808
|
header: VERSION_HEADER,
|
|
17357
17809
|
query: {
|
|
17358
17810
|
q: query,
|
|
17359
|
-
libraries:
|
|
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 =
|
|
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
|
-
|
|
17378
|
-
const asset = await fetchAsset(context,
|
|
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
|
-
|
|
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
|
|
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/${
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
17515
|
-
assets.command("get").description("Show a single asset").argument("<assetId>", "Asset id").
|
|
17516
|
-
assets.command("search").description("Search assets across the organization
|
|
17517
|
-
assets.command("download").description("Download an asset's file").argument("<assetId>", "Asset id").
|
|
17518
|
-
assets.command("upload").description("Upload local files as assets").argument("<files...>", "Local file paths").
|
|
17519
|
-
assets.command("rm").description("Move assets to trash (recoverable)").argument("<assetIds...>", "Asset ids").
|
|
17520
|
-
assets.command("describe").description("Set an asset's description").argument("<assetId>", "Asset id").requiredOption("--
|
|
17521
|
-
assets.command("transfer").description("Move assets to another library").
|
|
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
|
-
|
|
17530
|
-
|
|
17531
|
-
|
|
17532
|
-
|
|
17533
|
-
|
|
17534
|
-
if (!
|
|
17535
|
-
|
|
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
|
-
|
|
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({
|
|
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(
|
|
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
|
-
|
|
17550
|
-
|
|
17551
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
17588
|
-
const viewer = await
|
|
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("
|
|
17603
|
-
auth.command("logout").description("
|
|
17604
|
-
auth.command("status").description("Show which
|
|
17605
|
-
program2.command("whoami").description("Show the
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
17683
|
-
libraries.command("create").description("Create a library").requiredOption("--
|
|
17684
|
-
libraries.command("rename").description("Rename
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
18603
|
+
note(context, `Tagged ${assetIds.length} asset(s) with ${options.tag.join(", ")}.`);
|
|
17777
18604
|
}
|
|
17778
|
-
async function untagAssetsCommand(context, options) {
|
|
17779
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
17797
|
-
tags.command("add").description("Add tags to assets").
|
|
17798
|
-
tags.command("rm").description("Remove tags from assets").
|
|
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);
|