Skip to content

Commit

Permalink
Use the term origin to refer to scheme://host (#493)
Browse files Browse the repository at this point in the history
  • Loading branch information
visnup authored Jan 11, 2024
1 parent 842f276 commit 9488134
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 53 deletions.
16 changes: 8 additions & 8 deletions src/observableApiAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import {isatty} from "node:tty";
import open from "open";
import {HttpError, isHttpError} from "./error.js";
import type {Logger} from "./logger.js";
import {ObservableApiClient, getObservableUiHost} from "./observableApiClient.js";
import {ObservableApiClient, getObservableUiOrigin} from "./observableApiClient.js";
import {type ApiKey, getObservableApiKey, setObservableApiKey} from "./observableApiConfig.js";

const OBSERVABLE_UI_HOST = getObservableUiHost();
const OBSERVABLE_UI_ORIGIN = getObservableUiOrigin();

export const commandRequiresAuthenticationMessage = `You need to be authenticated to ${
getObservableUiHost().hostname
getObservableUiOrigin().hostname
} to run this command. Please run \`observable login\`.`;

/** Actions this command needs to take wrt its environment that may need mocked out. */
Expand Down Expand Up @@ -42,7 +42,7 @@ export async function login(effects = defaultEffects) {
const server = new LoginServer({nonce, effects});
await server.start();

const url = new URL("/settings/api-keys/generate", OBSERVABLE_UI_HOST);
const url = new URL("/settings/api-keys/generate", OBSERVABLE_UI_ORIGIN);
const name = `Observable CLI on ${os.hostname()}`;
const request = {
nonce,
Expand Down Expand Up @@ -78,7 +78,7 @@ export async function whoami(effects = defaultEffects) {
try {
const user = await apiClient.getCurrentUser();
logger.log();
logger.log(`You are logged into ${OBSERVABLE_UI_HOST.hostname} as ${formatUser(user)}.`);
logger.log(`You are logged into ${OBSERVABLE_UI_ORIGIN.hostname} as ${formatUser(user)}.`);
logger.log();
logger.log("You have access to the following workspaces:");
for (const workspace of user.workspaces) {
Expand Down Expand Up @@ -234,9 +234,9 @@ class LoginServer {
return false;
}
return (
parsedOrigin.protocol === OBSERVABLE_UI_HOST.protocol &&
parsedOrigin.host === OBSERVABLE_UI_HOST.host &&
parsedOrigin.port === OBSERVABLE_UI_HOST.port
parsedOrigin.protocol === OBSERVABLE_UI_ORIGIN.protocol &&
parsedOrigin.host === OBSERVABLE_UI_ORIGIN.host &&
parsedOrigin.port === OBSERVABLE_UI_ORIGIN.port
);
}
}
Expand Down
36 changes: 18 additions & 18 deletions src/observableApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,36 +28,36 @@ export interface GetProjectResponse {
servingRoot: string;
}

export function getObservableUiHost(env = process.env): URL {
const urlText = env["OBSERVABLE_HOST"] ?? "https://observablehq.com";
export function getObservableUiOrigin(env = process.env): URL {
const urlText = env["OBSERVABLE_ORIGIN"] ?? "https://observablehq.com";
try {
return new URL(urlText);
} catch (error) {
throw new CliError(`Invalid OBSERVABLE_HOST environment variable: ${error}`, {cause: error});
throw new CliError(`Invalid OBSERVABLE_ORIGIN environment variable: ${error}`, {cause: error});
}
}

export function getObservableApiHost(env = process.env): URL {
const urlText = env["OBSERVABLE_API_HOST"];
export function getObservableApiOrigin(env = process.env): URL {
const urlText = env["OBSERVABLE_API_ORIGIN"];
if (urlText) {
try {
return new URL(urlText);
} catch (error) {
throw new CliError(`Invalid OBSERVABLE_API_HOST environment variable: ${error}`, {cause: error});
throw new CliError(`Invalid OBSERVABLE_API_ORIGIN environment variable: ${error}`, {cause: error});
}
}

const uiHost = getObservableUiHost(env);
uiHost.hostname = "api." + uiHost.hostname;
return uiHost;
const uiOrigin = getObservableUiOrigin(env);
uiOrigin.hostname = "api." + uiOrigin.hostname;
return uiOrigin;
}

export class ObservableApiClient {
private _apiHeaders: Record<string, string>;
private _apiHost: URL;
private _apiOrigin: URL;

constructor({apiKey, apiHost = getObservableApiHost()}: {apiHost?: URL; apiKey: ApiKey}) {
this._apiHost = apiHost;
constructor({apiKey, apiOrigin = getObservableApiOrigin()}: {apiOrigin?: URL; apiKey: ApiKey}) {
this._apiOrigin = apiOrigin;
this._apiHeaders = {
Accept: "application/json",
Authorization: `apikey ${apiKey.key}`,
Expand Down Expand Up @@ -99,7 +99,7 @@ export class ObservableApiClient {
}

async getCurrentUser(): Promise<GetCurrentUserResponse> {
return await this._fetch<GetCurrentUserResponse>(new URL("/cli/user", this._apiHost), {method: "GET"});
return await this._fetch<GetCurrentUserResponse>(new URL("/cli/user", this._apiOrigin), {method: "GET"});
}

async getProject({
Expand All @@ -109,7 +109,7 @@ export class ObservableApiClient {
workspaceLogin: string;
projectSlug: string;
}): Promise<GetProjectResponse> {
const url = new URL(`/cli/project/@${workspaceLogin}/${projectSlug}`, this._apiHost);
const url = new URL(`/cli/project/@${workspaceLogin}/${projectSlug}`, this._apiOrigin);
return await this._fetch<GetProjectResponse>(url, {method: "GET"});
}

Expand All @@ -122,15 +122,15 @@ export class ObservableApiClient {
slug: string;
workspaceId: string;
}): Promise<PostProjectResponse> {
return await this._fetch<PostProjectResponse>(new URL("/cli/project", this._apiHost), {
return await this._fetch<PostProjectResponse>(new URL("/cli/project", this._apiOrigin), {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({title, slug, workspace: workspaceId})
});
}

async postDeploy({projectId, message}: {projectId: string; message: string}): Promise<string> {
const data = await this._fetch<{id: string}>(new URL(`/cli/project/${projectId}/deploy`, this._apiHost), {
const data = await this._fetch<{id: string}>(new URL(`/cli/project/${projectId}/deploy`, this._apiOrigin), {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({message})
Expand All @@ -145,7 +145,7 @@ export class ObservableApiClient {

async postDeployFileContents(deployId: string, contents: Buffer | string, relativePath: string): Promise<void> {
if (typeof contents === "string") contents = Buffer.from(contents);
const url = new URL(`/cli/deploy/${deployId}/file`, this._apiHost);
const url = new URL(`/cli/deploy/${deployId}/file`, this._apiOrigin);
const body = new FormData();
const blob = new Blob([contents]);
body.append("file", blob);
Expand All @@ -154,7 +154,7 @@ export class ObservableApiClient {
}

async postDeployUploaded(deployId: string): Promise<DeployInfo> {
return await this._fetch<DeployInfo>(new URL(`/cli/deploy/${deployId}/uploaded`, this._apiHost), {
return await this._fetch<DeployInfo>(new URL(`/cli/deploy/${deployId}/uploaded`, this._apiOrigin), {
method: "POST",
headers: {"content-type": "application/json"},
body: "{}"
Expand Down
4 changes: 2 additions & 2 deletions src/rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import esbuild from "rollup-plugin-esbuild";
import {nodeResolve} from "@rollup/plugin-node-resolve";
import {getStringLiteralValue, isStringLiteral} from "./javascript/features.js";
import {isPathImport, resolveNpmImport} from "./javascript/imports.js";
import {getObservableUiHost} from "./observableApiClient.js";
import {getObservableUiOrigin} from "./observableApiClient.js";
import {Sourcemap} from "./sourcemap.js";
import {relativeUrl} from "./url.js";

Expand Down Expand Up @@ -85,7 +85,7 @@ export async function rollupClient(clientPath: string, {minify = false} = {}): P
exclude: [], // don’t exclude node_modules
minify,
define: {
"process.env.OBSERVABLE_ORIGIN": JSON.stringify(String(getObservableUiHost()).replace(/\/$/, ""))
"process.env.OBSERVABLE_ORIGIN": JSON.stringify(String(getObservableUiOrigin()).replace(/\/$/, ""))
}
}),
importMetaResolve()
Expand Down
10 changes: 7 additions & 3 deletions test/mocks/observableApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {type Dispatcher, type Interceptable, MockAgent, getGlobalDispatcher, setGlobalDispatcher} from "undici";
import {getObservableApiHost, getObservableUiHost} from "../../src/observableApiClient.js";
import {getObservableApiOrigin, getObservableUiOrigin} from "../../src/observableApiClient.js";

export const validApiKey = "MOCK-VALID-KEY";
export const invalidApiKey = "MOCK-INVALID-KEY";
Expand All @@ -14,7 +14,7 @@ export class ObservableApiMock {
public start(): ObservableApiMock {
this._agent = new MockAgent();
this._agent.disableNetConnect();
const origin = getObservableApiHost().toString().replace(/\/$/, "");
const origin = getObservableApiOrigin().toString().replace(/\/$/, "");
const mockPool = this._agent.get(origin);
for (const handler of this._handlers) handler(mockPool);
this._originalDispatcher = getGlobalDispatcher();
Expand Down Expand Up @@ -126,7 +126,11 @@ export class ObservableApiMock {
handlePostDeployUploaded({deployId, status = 200}: {deployId?: string; status?: number} = {}): ObservableApiMock {
const response =
status == 200
? JSON.stringify({id: deployId, status: "uploaded", url: `${getObservableUiHost()}/@mock-user-ws/test-project`})
? JSON.stringify({
id: deployId,
status: "uploaded",
url: `${getObservableUiOrigin()}/@mock-user-ws/test-project`
})
: emptyErrorBody;
const headers = authorizationHeader(status != 401);
this._handlers.push((pool) =>
Expand Down
49 changes: 27 additions & 22 deletions test/observableApiClient-test.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,58 @@
import assert from "assert";
import {CliError} from "../src/error.js";
import {getObservableApiHost, getObservableUiHost} from "../src/observableApiClient.js";
import {getObservableApiOrigin, getObservableUiOrigin} from "../src/observableApiClient.js";

describe("getObservableUiHost", () => {
describe("getObservableUiOrigin", () => {
it("works", () => {
assert.deepEqual(getObservableUiHost({OBSERVABLE_HOST: "https://example.com"}), new URL("https://example.com"));
assert.equal(
String(getObservableUiOrigin({OBSERVABLE_ORIGIN: "https://example.com"})),
String(new URL("https://example.com"))
);
});

it("throws an appropriate error for malformed URLs", () => {
try {
getObservableUiHost({OBSERVABLE_HOST: "bad url"});
getObservableUiOrigin({OBSERVABLE_ORIGIN: "bad url"});
assert.fail("expected error");
} catch (error) {
CliError.assert(error, {message: /^Invalid OBSERVABLE_HOST environment variable: /});
CliError.assert(error, {message: /^Invalid OBSERVABLE_ORIGIN environment variable: /});
}
});
});

describe("getObservableApiHost", () => {
describe("getObservableApiOrigin", () => {
it("works", () => {
assert.deepEqual(
getObservableApiHost({OBSERVABLE_API_HOST: "https://example.com"}),
new URL("https://example.com")
assert.equal(
String(getObservableApiOrigin({OBSERVABLE_API_ORIGIN: "https://example.com"})),
String(new URL("https://example.com"))
);
});

it("throws an appropriate error for malformed URLs", () => {
try {
getObservableApiHost({OBSERVABLE_API_HOST: "bad url"});
getObservableApiOrigin({OBSERVABLE_API_ORIGIN: "bad url"});
assert.fail("expected error");
} catch (error) {
CliError.assert(error, {message: /^Invalid OBSERVABLE_API_HOST environment variable: /});
CliError.assert(error, {message: /^Invalid OBSERVABLE_API_ORIGIN environment variable: /});
}
});

it("prefers OBSERVABLE_API_HOST", () => {
assert.deepEqual(
getObservableApiHost({
OBSERVABLE_API_HOST: "https://example.com/api",
OBSERVABLE_HOST: "https://example.com/ui"
}),
new URL("https://example.com/api")
it("prefers OBSERVABLE_API_ORIGIN", () => {
assert.equal(
String(
getObservableApiOrigin({
OBSERVABLE_API_ORIGIN: "https://api.example.com",
OBSERVABLE_ORIGIN: "https://ui.example.com"
})
),
String(new URL("https://api.example.com"))
);
});

it("falls back to OBSERVABLE_HOST", () => {
assert.deepEqual(
getObservableApiHost({OBSERVABLE_API_HOST: "https://example.com/api"}),
new URL("https://example.com/ui")
it("falls back to OBSERVABLE_ORIGIN", () => {
assert.equal(
String(getObservableApiOrigin({OBSERVABLE_API_ORIGIN: "", OBSERVABLE_ORIGIN: "https://example.com"})),
String(new URL("https://api.example.com"))
);
});
});

0 comments on commit 9488134

Please sign in to comment.