|
| 1 | +import type {ResolvedContext} from '../base.js' |
| 2 | + |
| 3 | +/** |
| 4 | + * The admin portal URL of one environment (gateway N49): |
| 5 | + * `GET|PATCH /v3/account/projects/environments/{environmentName}/admin-url`. |
| 6 | + * The SDK does not know these routes yet, so the commands call them directly |
| 7 | + * with the same headers the SDK transport sends. |
| 8 | + */ |
| 9 | + |
| 10 | +/** One environment's admin portal URL as the hub answers it. */ |
| 11 | +export interface EnvironmentAdminUrl { |
| 12 | + env: string |
| 13 | + /** The environment's own URL; absent = it uses the fallback. */ |
| 14 | + adminUrl?: string |
| 15 | + /** The base its password-reset / magic-link / invitation e-mails open. */ |
| 16 | + effectiveAdminUrl?: string |
| 17 | + /** environment / project / hosted / cloud / none */ |
| 18 | + source: string |
| 19 | + /** Why a set value was skipped, or why the links cannot be opened. */ |
| 20 | + note?: string |
| 21 | +} |
| 22 | + |
| 23 | +export interface HubCall { |
| 24 | + method: 'GET' | 'PATCH' |
| 25 | + url: string |
| 26 | + headers: Record<string, string> |
| 27 | + body?: unknown |
| 28 | +} |
| 29 | + |
| 30 | +interface HubAnswer { |
| 31 | + item?: EnvironmentAdminUrl |
| 32 | + responseStatus?: {isSuccess?: boolean; errors?: Array<{message?: string; errorCode?: string}>} |
| 33 | +} |
| 34 | + |
| 35 | +/** The environment a command acts on: --env / --prod / the saved default, else PROD. */ |
| 36 | +export function targetEnv(ctx: Pick<ResolvedContext, 'env'>): string { |
| 37 | + return (ctx.env ?? 'PROD').trim().toUpperCase() |
| 38 | +} |
| 39 | + |
| 40 | +/** Builds the request; the caller checked that a project and a token are there. */ |
| 41 | +export function envAdminUrlCall( |
| 42 | + ctx: ResolvedContext, |
| 43 | + token: string, |
| 44 | + env: string, |
| 45 | + method: 'GET' | 'PATCH', |
| 46 | + url?: string, |
| 47 | +): HubCall { |
| 48 | + const href = `${ctx.hubUrl.replace(/\/$/, '')}/v3/account/projects/environments/${encodeURIComponent(env)}/admin-url` |
| 49 | + const target = new URL(href) |
| 50 | + // The route has no {projectId} segment: the project rides in the query, as for the env list. |
| 51 | + target.searchParams.set('projectId', ctx.projectId!) |
| 52 | + const headers: Record<string, string> = { |
| 53 | + Accept: 'application/json', |
| 54 | + Authorization: `Bearer ${token}`, |
| 55 | + 'norbix-project-id': ctx.projectId!, |
| 56 | + } |
| 57 | + if (method === 'PATCH') headers['Content-Type'] = 'application/json' |
| 58 | + if (ctx.accountId) headers['norbix-account-id'] = ctx.accountId |
| 59 | + if (ctx.region) headers['nb-region'] = ctx.region |
| 60 | + return method === 'PATCH' |
| 61 | + ? {method, url: target.href, headers, body: {environmentName: env, url: url ?? ''}} |
| 62 | + : {method, url: target.href, headers} |
| 63 | +} |
| 64 | + |
| 65 | +/** Sends the call; throws an Error with the hub's reason (and a hint on an old hub). */ |
| 66 | +export async function sendEnvAdminUrlCall(call: HubCall): Promise<HubAnswer> { |
| 67 | + const res = await fetch(call.url, { |
| 68 | + method: call.method, |
| 69 | + headers: call.headers, |
| 70 | + body: call.body === undefined ? undefined : JSON.stringify(call.body), |
| 71 | + }) |
| 72 | + const text = await res.text() |
| 73 | + let answer: HubAnswer = {} |
| 74 | + try { |
| 75 | + answer = text ? (JSON.parse(text) as HubAnswer) : {} |
| 76 | + } catch { |
| 77 | + throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`) |
| 78 | + } |
| 79 | + |
| 80 | + if (!res.ok || answer.responseStatus?.isSuccess === false) { |
| 81 | + const why = answer.responseStatus?.errors?.map((e) => [e.errorCode, e.message].filter(Boolean).join(' ')).join('; ') |
| 82 | + const old = res.status === 404 || res.status === 405 |
| 83 | + throw new Error( |
| 84 | + `HTTP ${res.status}${why ? `: ${why}` : ''}${old && !why ? ' (this hub is older than the per-environment admin URL — upgrade it)' : ''}`, |
| 85 | + ) |
| 86 | + } |
| 87 | + |
| 88 | + return answer |
| 89 | +} |
| 90 | + |
| 91 | +/** One line for people: where the env's e-mail links point and why. */ |
| 92 | +export function describeEnvAdminUrl(a: EnvironmentAdminUrl): string { |
| 93 | + const from: Record<string, string> = { |
| 94 | + environment: 'its own admin URL', |
| 95 | + project: 'the project admin URL (no URL of its own)', |
| 96 | + hosted: 'the Norbix-hosted portal', |
| 97 | + cloud: 'the Cloud URL — the Cloud cannot open these links', |
| 98 | + none: 'nothing — the links are relative', |
| 99 | + } |
| 100 | + const line = `${a.env}: e-mail links open ${a.effectiveAdminUrl ?? '(no address)'} — from ${from[a.source] ?? a.source}` |
| 101 | + return a.note ? `${line}\n note: ${a.note}` : line |
| 102 | +} |
0 commit comments