Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,12 @@ so it can be inspected from a pipe; anything else is written to stderr only.

### Environment variables

Everything `seam login`, `seam select workspace`, and `seam select server`
Everything `seam login`, `seam select workspace`, and `seam select endpoint`
store may be given in the environment instead:

- `SEAM_CLI_TOKEN`: a Personal Access Token or API Key,
- `SEAM_CLI_WORKSPACE_ID`: the workspace requests are made against,
- `SEAM_CLI_ENDPOINT`: the Seam API server requests are made to.
- `SEAM_CLI_ENDPOINT`: the Seam API endpoint requests are made to.

Any of them, all of them, or none of them may be set. Each one wins over the
corresponding stored value, which makes them useful for CI, for a single
Expand All @@ -213,7 +213,7 @@ Personal Access Token works across workspaces, so it needs one from either
The command that would store an overridden value fails rather than storing
something the environment ignores: `seam login` and `seam logout` while
`SEAM_CLI_TOKEN` is set, `seam select workspace` while
`SEAM_CLI_WORKSPACE_ID` is set, and `seam select server` while
`SEAM_CLI_WORKSPACE_ID` is set, and `seam select endpoint` while
`SEAM_CLI_ENDPOINT` is set. Unset the variable to use those commands.

```bash
Expand Down Expand Up @@ -278,7 +278,7 @@ the loaders for all three shells.
Completions are generated from the cached Seam API definitions, so they may
briefly lag a newly released API. Pass `--update` to refresh the cache first,
e.g., `seam completion bash --update`. They do not reflect definitions served
by another Seam API server when `seam config use-remote-api-defs` is enabled.
by another Seam API endpoint when `seam config use-remote-api-defs` is enabled.

If completions do not appear after installing them system wide:

Expand Down
2 changes: 1 addition & 1 deletion TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ request is the product. "The prompt offered these choices with these hints" is
behavior: the choices are what the user sees
(`blueprint-object.test.ts` asserting on the recorded `choices`
is the good in-repo example). "`resolveAuth` called `getConfigStore`" is
implementation: the contract is _what server comes back_, not how it was
implementation: the contract is _what endpoint comes back_, not how it was
looked up.

Even at a real boundary, prefer **capture-then-assert** over
Expand Down
4 changes: 2 additions & 2 deletions src/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,12 @@ async function cli(args: ParsedArgs, argv: string[]) {
const localCommand = findLocalCommand(args._)

// Commands declared not to need a token bypass the login gate. A partial
// path keeps the historical rule: only login and select server may be
// path keeps the historical rule: only login and select endpoint may be
// reached logged out.
const requiresAuth =
localCommand != null
? localCommand.requiresAuth
: !(args._[0] === 'login' || isEqual(args._, ['select', 'server']))
: !(args._[0] === 'login' || isEqual(args._, ['select', 'endpoint']))

if (requiresAuth && resolveAuth(config).token == null) {
output.error(`Not logged in. Please run "seam login" or set ${tokenEnvVar}`)
Expand Down
14 changes: 9 additions & 5 deletions src/lib/args/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,15 @@ test('assertKnownArgs: names an endpoint command by its path', () => {

test('assertKnownArgs: names a CLI command by its words', () => {
expect(() => {
assertKnownArgs({ serverr: 'https://example.com' }, ['select', 'server'], {
accepted: new Set(['server']),
isLocal: true,
})
}).toThrow('Unknown parameter for select server: --serverr')
assertKnownArgs(
{ endpointt: 'https://example.com' },
['select', 'endpoint'],
{
accepted: new Set(['endpoint']),
isLocal: true,
},
)
}).toThrow('Unknown parameter for select endpoint: --endpointt')
})

test('assertKnownArgs: names every unknown argument at once, with a hint', () => {
Expand Down
78 changes: 43 additions & 35 deletions src/lib/auth/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { validateToken } from './validate-token.js'

/** A stored auth setting an environment variable may override. */
export type AuthSetting = 'server' | 'token' | 'workspaceId'
export type AuthSetting = 'endpoint' | 'token' | 'workspaceId'

/**
* Refuse to store a setting the environment overrides.
Expand All @@ -29,10 +29,10 @@ export const assertMutable = (
action: string,
): void => {
const { envVar, source, value } = {
server: {
endpoint: {
envVar: endpointEnvVar,
source: auth.serverSource,
value: auth.server,
source: auth.endpointSource,
value: auth.endpoint,
},
token: { envVar: tokenEnvVar, source: auth.tokenSource, value: auth.token },
workspaceId: {
Expand All @@ -47,21 +47,21 @@ export const assertMutable = (
}

export interface LoginOptions {
server?: string | undefined
endpoint?: string | undefined
token?: string | undefined
workspaceId?: string | undefined
}

/**
* Store the given credentials, validating the token first.
*
* The token is stored under the server it will be used with, so a given
* server is stored and re-resolved before the token key is derived.
* The token is stored under the endpoint it will be used with, so a given
* endpoint is stored and re-resolved before the token key is derived.
*
* Validation reaches the network, so a test may inject its own `validate`.
*/
export const login = async (
{ server, token, workspaceId }: LoginOptions,
{ endpoint, token, workspaceId }: LoginOptions,
config: ConfigStore = getConfigStore(),
validate: typeof validateToken = validateToken,
): Promise<void> => {
Expand All @@ -70,20 +70,19 @@ export const login = async (
// Nothing is stored while the environment overrides it, so refuse before
// storing anything rather than part way through.
assertMutable(auth, 'token', 'log in')
if (server != null) assertMutable(auth, 'server', 'select a server')
if (endpoint != null) assertMutable(auth, 'endpoint', 'select an endpoint')
if (workspaceId != null) {
assertMutable(auth, 'workspaceId', 'select a workspace')
}

if (server != null) {
config.set('server', server)
config.delete('current_workspace_id')
if (endpoint != null) {
storeEndpoint(endpoint, config)
auth = resolveAuth(config)
}

if (token != null) {
await validate(token, workspaceId)
config.set(`${auth.server}.pat`, token)
config.set(`${auth.endpoint}.pat`, token)
config.delete('current_workspace_id')
}

Expand All @@ -92,39 +91,38 @@ export const login = async (
}
}

/** Store the token for the current server, e.g., one just prompted for. */
/** Store the token for the current endpoint, e.g., one just prompted for. */
export const storeToken = (
token: string,
config: ConfigStore = getConfigStore(),
): void => {
const auth = resolveAuth(config)
assertMutable(auth, 'token', 'log in')
config.set(`${auth.server}.pat`, token)
config.set(`${auth.endpoint}.pat`, token)
}

/** Remove the stored token and workspace selection. */
export const logout = (config: ConfigStore = getConfigStore()): void => {
const auth = resolveAuth(config)
assertMutable(auth, 'token', 'log out')
config.delete(`${auth.server}.pat`)
// Configs written before tokens were stored per server may still hold an
config.delete(`${auth.endpoint}.pat`)
// Configs written before tokens were stored per endpoint may still hold an
// un-namespaced token, so drop that too.
config.delete('pat')
config.delete('current_workspace_id')
}

/**
* Store the server to make requests against.
* Store the endpoint to make requests against.
*
* The workspace selection belongs to the previous server, so it is cleared.
* The workspace selection belongs to the previous endpoint, so it is cleared.
*/
export const selectServer = (
server: string,
export const selectEndpoint = (
endpoint: string,
config: ConfigStore = getConfigStore(),
): void => {
assertMutable(resolveAuth(config), 'server', 'select a server')
config.set('server', server)
config.delete('current_workspace_id')
assertMutable(resolveAuth(config), 'endpoint', 'select an endpoint')
storeEndpoint(endpoint, config)
}

/** Store the workspace requests are made against. */
Expand All @@ -137,33 +135,43 @@ export const selectWorkspace = (
}

/**
* Point the CLI at a fake Seam Connect server and store the well-known
* token it accepts. Returns the generated server URL for reporting.
* Point the CLI at a fake Seam Connect endpoint and store the well-known
* token it accepts. Returns the generated endpoint URL for reporting.
*/
export const selectFakeServer = ({
export const selectFakeEndpoint = ({
urlSeed = randomBytes(5).toString('hex'),
config = getConfigStore(),
}: {
urlSeed?: string
config?: ConfigStore
} = {}): { server: string; token: string } => {
} = {}): { endpoint: string; token: string } => {
const auth = resolveAuth(config)
assertMutable(auth, 'server', 'select a server')
assertMutable(auth, 'endpoint', 'select an endpoint')
assertMutable(auth, 'token', 'log in')

const server = `https://${urlSeed}.fakeseamconnect.seam.vc`
const endpoint = `https://${urlSeed}.fakeseamconnect.seam.vc`
const token = 'seam_apikey1_token'
config.set('server', server)
config.set(`${server}.pat`, token)
config.delete('current_workspace_id')
storeEndpoint(endpoint, config)
config.set(`${endpoint}.pat`, token)

return { server, token }
return { endpoint, token }
}

/** Store whether API definitions come from the server instead of npm. */
/** Store whether API definitions come from the endpoint instead of npm. */
export const setUseRemoteApiDefs = (
useRemoteApiDefs: boolean,
config: ConfigStore = getConfigStore(),
): void => {
config.set('use_remote_api_defs', useRemoteApiDefs)
}

/**
* Write the endpoint, dropping what belonged to the previous one: the
* workspace selection, and any value left under the legacy `server` key that
* {@link resolveAuth} would otherwise still fall back to.
*/
const storeEndpoint = (endpoint: string, config: ConfigStore): void => {
config.set('endpoint', endpoint)
config.delete('server')
config.delete('current_workspace_id')
}
2 changes: 1 addition & 1 deletion src/lib/auth/validate-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { resolveAuth } from 'lib/context.js'

export const validateToken = async (token: string, workspaceId?: string) => {
const options = { endpoint: resolveAuth().server }
const options = { endpoint: resolveAuth().endpoint }

if (isPersonalAccessToken(token)) {
const seam = workspaceId
Expand Down
6 changes: 3 additions & 3 deletions src/lib/blueprint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type ApiBlueprint = Blueprint

export interface GetApiBlueprintOptions {
/**
* Build from the OpenAPI document the configured server is currently
* Build from the OpenAPI document the configured endpoint is currently
* running, instead of the published npm types.
*/
useRemoteDefinitions?: boolean
Expand All @@ -19,8 +19,8 @@ export const getApiBlueprint = async ({
useRemoteDefinitions = false,
update = false,
}: GetApiBlueprintOptions = {}): Promise<ApiBlueprint> => {
// Remote definitions describe whatever the server is currently running, so
// build them directly from the server's OpenAPI document.
// Remote definitions describe whatever the endpoint is currently running, so
// build them directly from that endpoint's OpenAPI document.
if (useRemoteDefinitions) return await createRemoteBlueprint()

return await getBlueprint({ update })
Expand Down
6 changes: 3 additions & 3 deletions src/lib/blueprint/source-remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import type { Blueprint } from '@seamapi/blueprint'
import { resolveAuth } from 'lib/context.js'

/**
* Build a blueprint from the OpenAPI document the current server is running,
* describing exactly what that server accepts rather than what is published.
* Build a blueprint from the OpenAPI document the current endpoint is running,
* describing exactly what that endpoint accepts rather than what is published.
*/
export const createRemoteBlueprint = async (): Promise<Blueprint> => {
const [{ createBlueprint }, { getOpenapiSchema }] = await Promise.all([
import('@seamapi/blueprint'),
import('@seamapi/http/connect'),
])
const openapi = await getOpenapiSchema(resolveAuth().server)
const openapi = await getOpenapiSchema(resolveAuth().endpoint)

return await createBlueprint({ openapi }, { omitUndocumented: true })
}
21 changes: 0 additions & 21 deletions src/lib/commands/local/config-set-fake-server.ts

This file was deleted.

6 changes: 3 additions & 3 deletions src/lib/commands/local/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ export const loginCommand: Command = {
description:
'Prompts for a personal access token unless one is passed with --token.',
flags: [
stringFlag('server', 'Seam API server to log in to.'),
stringFlag('endpoint', 'Seam API endpoint to log in to.'),
stringFlag('token', 'Personal access token to log in with.'),
stringFlag('workspace-id', 'Workspace to select after logging in.'),
],
},
requiresAuth: false,
execute: async ({ args }, ctx) => {
if (args['token'] || args['workspace_id'] || args['server']) {
if (args['token'] || args['workspace_id'] || args['endpoint']) {
await login(
{
server: args['server'] ? args['server'] : undefined,
endpoint: args['endpoint'] ? args['endpoint'] : undefined,
token: args['token'] ? String(args['token']).trim() : undefined,
workspaceId: args['workspace_id'] ? args['workspace_id'] : undefined,
},
Expand Down
30 changes: 30 additions & 0 deletions src/lib/commands/local/select-endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { assertMutable, selectEndpoint } from 'lib/auth/operations.js'
import type { Command } from 'lib/commands/registry.js'
import { stringFlag } from 'lib/commands/spec.js'
import { NonInteractiveError } from 'lib/errors.js'
import { interactForEndpointSelection } from 'lib/interactions/index.js'

export const selectEndpointCommand: Command = {
definition: {
path: ['select', 'endpoint'],
kind: 'cli',
title: 'Select the Seam API endpoint.',
description: '',
flags: [stringFlag('endpoint', 'Seam API endpoint to select.')],
},
requiresAuth: false,
execute: async ({ args }, ctx) => {
assertMutable(ctx.auth, 'endpoint', 'select an endpoint')
if (args['endpoint']) {
selectEndpoint(args['endpoint'], ctx.config)
return { kind: 'done' }
}
if (ctx.interactivity === 'non-interactive') {
throw new NonInteractiveError(
'Missing required parameter for select endpoint: --endpoint',
)
}
await interactForEndpointSelection()
return { kind: 'done' }
},
}
Loading
Loading