mirror of
https://github.com/hanzoai/id.git
synced 2026-10-16 18:38:15 +00:00
id-dev.hanzo.ai is the devnet's identity host, and stays on the devnet
The catalog gains id-dev.hanzo.ai: org hanzo on network devnet, client hanzo-console, its issuer and /v1 its own origin (the dev cloud's IAM), its app https://app-dev.hanzo.ai, its pay surface https://app-dev.hanzo.ai/pay and its telemetry api-dev.hanzo.ai. Three things in the portal sent an org-hanzo host to mainnet, and each now reads the host's own entry: - the alias redirect read one front door per org, so id-dev was sent to hanzo.id with its OAuth request; front doors are now per org per network, and a network with none stays put; - a signed-in visit went to the brand's first app (hanzo.ai), the launcher, billing and the organization page linked mainnet; a host whose entry names `appUrl` links only its network's app and payUrl; - telemetry went to api.hanzo.ai; an entry's `apiUrl` names its gateway. Every mainnet entry names none of the new fields, so every mainnet host resolves, redirects and links exactly as before. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -1 +1 @@
|
||||
{"iamTenantConfigJson":"{\n \"hanzo.id\": {\"orgId\":\"hanzo\",\"canonical\":true,\"clientId\":\"hanzo-console\",\"appName\":\"hanzo-console\",\"brandUrl\":\"https://cdn.jsdelivr.net/npm/@hanzo/[email protected]/brand.json\"},\n \"iam.hanzo.ai\": {\"orgId\":\"hanzo\",\"clientId\":\"hanzo-console\",\"appName\":\"hanzo-console\",\"iamUrl\":\"https://iam.hanzo.ai\",\"publicOrigin\":\"https://iam.hanzo.ai\",\"brandUrl\":\"https://cdn.jsdelivr.net/npm/@hanzo/[email protected]/brand.json\"},\n \"auth.hanzo.ai\": {\"orgId\":\"hanzo\",\"clientId\":\"hanzo-console\",\"appName\":\"hanzo-console\",\"iamUrl\":\"https://auth.hanzo.ai\",\"publicOrigin\":\"https://auth.hanzo.ai\",\"brandUrl\":\"https://cdn.jsdelivr.net/npm/@hanzo/[email protected]/brand.json\"},\n \"lux.id\": {\"orgId\":\"lux\", \"canonical\":true,\"payUrl\":\"https://pay.lux.cloud\",\"clientId\":\"lux-cloud\", \"appName\":\"lux-cloud\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@luxfi/[email protected]/brand.json\"},\n \"id.lux.network\": {\"orgId\":\"lux\", \"payUrl\":\"https://pay.lux.cloud\",\"clientId\":\"lux-cloud\", \"appName\":\"lux-cloud\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@luxfi/[email protected]/brand.json\"},\n \"zoolabs.id\": {\"orgId\":\"zoo\", \"canonical\":true,\"payUrl\":\"https://pay.zoo.cloud\",\"clientId\":\"zoo-console\", \"appName\":\"zoo-console\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@zooai/[email protected]/brand.json\"},\n \"www.zoolabs.id\": {\"orgId\":\"zoo\", \"payUrl\":\"https://pay.zoo.cloud\",\"clientId\":\"zoo-console\", \"appName\":\"zoo-console\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@zooai/[email protected]/brand.json\"},\n \"id.zoo.network\": {\"orgId\":\"zoo\", \"payUrl\":\"https://pay.zoo.cloud\",\"clientId\":\"zoo-console\", \"appName\":\"zoo-console\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@zooai/[email protected]/brand.json\"},\n \"pars.id\": {\"orgId\":\"pars\", \"canonical\":true,\"clientId\":\"pars-console\", \"appName\":\"pars-console\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@parsdao/[email protected]/brand.json\"},\n \"id.pars.network\": {\"orgId\":\"pars\", \"clientId\":\"pars-console\", \"appName\":\"pars-console\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@parsdao/[email protected]/brand.json\"},\n \"osage.id\": {\"orgId\":\"osage\",\"canonical\":true,\"clientId\":\"osage-id-portal\",\"appName\":\"osage-id\"},\n \"www.osage.id\": {\"orgId\":\"osage\",\"clientId\":\"osage-id-portal\",\"appName\":\"osage-id\"},\n \"id.bootno.de\": {\"orgId\":\"bootnode\",\"canonical\":true,\"clientId\":\"bootnode-platform\",\"appName\":\"bootnode-platform\"},\n \"id.adnexus.dev\": {\"orgId\":\"adnexus\",\"canonical\":true,\"clientId\":\"adnexus-app\",\"appName\":\"adnexus-app\"}\n}\n","ingestKeyring":"{\n \"hanzo\": \"pk-rM_CdaF2MQckGCrla113SrR1oH4zvqN8xh2I95Z9tY8\",\n \"lux\": \"pk-gUZp6ZVfhJzSwK-rb4oLbVkpCnMBx5uSCpxf_5yEhQk\",\n \"zoo\": \"pk-3TKpKnERV9AQSsBUERWkZejC1O1mUxc1jRzsP3MPbs4\"\n}\n","v":1}
|
||||
{"iamTenantConfigJson":"{\n \"hanzo.id\": {\"orgId\":\"hanzo\",\"canonical\":true,\"clientId\":\"hanzo-console\",\"appName\":\"hanzo-console\",\"brandUrl\":\"https://cdn.jsdelivr.net/npm/@hanzo/[email protected]/brand.json\"},\n \"iam.hanzo.ai\": {\"orgId\":\"hanzo\",\"clientId\":\"hanzo-console\",\"appName\":\"hanzo-console\",\"iamUrl\":\"https://iam.hanzo.ai\",\"publicOrigin\":\"https://iam.hanzo.ai\",\"brandUrl\":\"https://cdn.jsdelivr.net/npm/@hanzo/[email protected]/brand.json\"},\n \"auth.hanzo.ai\": {\"orgId\":\"hanzo\",\"clientId\":\"hanzo-console\",\"appName\":\"hanzo-console\",\"iamUrl\":\"https://auth.hanzo.ai\",\"publicOrigin\":\"https://auth.hanzo.ai\",\"brandUrl\":\"https://cdn.jsdelivr.net/npm/@hanzo/[email protected]/brand.json\"},\n \"id-dev.hanzo.ai\": {\"orgId\":\"hanzo\",\"network\":\"devnet\",\"clientId\":\"hanzo-console\",\"appName\":\"hanzo-console\",\"iamUrl\":\"https://id-dev.hanzo.ai\",\"publicOrigin\":\"https://id-dev.hanzo.ai\",\"appUrl\":\"https://app-dev.hanzo.ai\",\"apiUrl\":\"https://api-dev.hanzo.ai\",\"payUrl\":\"https://app-dev.hanzo.ai/pay\",\"brandUrl\":\"https://cdn.jsdelivr.net/npm/@hanzo/[email protected]/brand.json\"},\n \"lux.id\": {\"orgId\":\"lux\", \"canonical\":true,\"payUrl\":\"https://pay.lux.cloud\",\"clientId\":\"lux-cloud\", \"appName\":\"lux-cloud\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@luxfi/[email protected]/brand.json\"},\n \"id.lux.network\": {\"orgId\":\"lux\", \"payUrl\":\"https://pay.lux.cloud\",\"clientId\":\"lux-cloud\", \"appName\":\"lux-cloud\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@luxfi/[email protected]/brand.json\"},\n \"zoolabs.id\": {\"orgId\":\"zoo\", \"canonical\":true,\"payUrl\":\"https://pay.zoo.cloud\",\"clientId\":\"zoo-console\", \"appName\":\"zoo-console\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@zooai/[email protected]/brand.json\"},\n \"www.zoolabs.id\": {\"orgId\":\"zoo\", \"payUrl\":\"https://pay.zoo.cloud\",\"clientId\":\"zoo-console\", \"appName\":\"zoo-console\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@zooai/[email protected]/brand.json\"},\n \"id.zoo.network\": {\"orgId\":\"zoo\", \"payUrl\":\"https://pay.zoo.cloud\",\"clientId\":\"zoo-console\", \"appName\":\"zoo-console\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@zooai/[email protected]/brand.json\"},\n \"pars.id\": {\"orgId\":\"pars\", \"canonical\":true,\"clientId\":\"pars-console\", \"appName\":\"pars-console\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@parsdao/[email protected]/brand.json\"},\n \"id.pars.network\": {\"orgId\":\"pars\", \"clientId\":\"pars-console\", \"appName\":\"pars-console\", \"brandUrl\":\"https://cdn.jsdelivr.net/npm/@parsdao/[email protected]/brand.json\"},\n \"osage.id\": {\"orgId\":\"osage\",\"canonical\":true,\"clientId\":\"osage-id-portal\",\"appName\":\"osage-id\"},\n \"www.osage.id\": {\"orgId\":\"osage\",\"clientId\":\"osage-id-portal\",\"appName\":\"osage-id\"},\n \"id.bootno.de\": {\"orgId\":\"bootnode\",\"canonical\":true,\"clientId\":\"bootnode-platform\",\"appName\":\"bootnode-platform\"},\n \"id.adnexus.dev\": {\"orgId\":\"adnexus\",\"canonical\":true,\"clientId\":\"adnexus-app\",\"appName\":\"adnexus-app\"}\n}\n","ingestKeyring":"{\n \"hanzo\": \"pk-rM_CdaF2MQckGCrla113SrR1oH4zvqN8xh2I95Z9tY8\",\n \"lux\": \"pk-gUZp6ZVfhJzSwK-rb4oLbVkpCnMBx5uSCpxf_5yEhQk\",\n \"zoo\": \"pk-3TKpKnERV9AQSsBUERWkZejC1O1mUxc1jRzsP3MPbs4\"\n}\n","v":1}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Org } from '@hanzo/id-shared'
|
||||
import { Empty, Section } from './ui'
|
||||
import { appsFor, billingFor } from '../marketing'
|
||||
import { appsOf, billingOf } from '../marketing'
|
||||
|
||||
/**
|
||||
* Everything this account opens, and where the money is.
|
||||
@@ -13,8 +13,8 @@ import { appsFor, billingFor } from '../marketing'
|
||||
* it actually is.
|
||||
*/
|
||||
export function Apps({ org }: { org: Org }) {
|
||||
const apps = appsFor(org.orgId)
|
||||
const billing = billingFor(org.orgId) ?? org.payUrl
|
||||
const apps = appsOf(org)
|
||||
const billing = billingOf(org) ?? org.payUrl
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { createAnalytics, type Transport } from '@hanzo/event'
|
||||
import { AnalyticsProvider as EventProvider, usePageview } from '@hanzo/event/react'
|
||||
import { ingestKey, loadRuntime, resolveOrg } from '@hanzo/id-shared'
|
||||
|
||||
/** The gateway telemetry goes to when the host's catalog entry names none (`apiUrl`). */
|
||||
const HOST = 'https://api.hanzo.ai'
|
||||
const PRODUCT = 'id'
|
||||
|
||||
@@ -255,7 +256,8 @@ export function Analytics({ children }: { children: ReactNode }) {
|
||||
void loadRuntime().then((runtime) => {
|
||||
if (cancelled) return
|
||||
const host = window.location.hostname
|
||||
const org = resolveOrg(host, { catalog: runtime.catalog }).orgId
|
||||
const resolved = resolveOrg(host, { catalog: runtime.catalog })
|
||||
const org = resolved.orgId
|
||||
const key = ingestKey(org, runtime.keyring)
|
||||
if (!key) {
|
||||
console.error(
|
||||
@@ -263,7 +265,7 @@ export function Analytics({ children }: { children: ReactNode }) {
|
||||
)
|
||||
return
|
||||
}
|
||||
setClient(createAnalytics({ product: PRODUCT, host: HOST, ingestKey: key, enabled: true, transport }))
|
||||
setClient(createAnalytics({ product: PRODUCT, host: resolved.apiUrl || HOST, ingestKey: key, enabled: true, transport }))
|
||||
setLive(true)
|
||||
})
|
||||
return () => {
|
||||
|
||||
@@ -34,3 +34,24 @@ test('the app launcher is unaffected', () => {
|
||||
assert.ok(appsFor(brand).length > 0)
|
||||
}
|
||||
})
|
||||
|
||||
// A host on another network (the devnet's id-dev.hanzo.ai) names its own app in
|
||||
// the catalog, and every link the portal draws for it stays on that network.
|
||||
test('a host that names its network\'s app links only there', async () => {
|
||||
const { appsOf, billingOf, teamOf } = await import('./marketing')
|
||||
const devnet = {
|
||||
orgId: 'hanzo',
|
||||
network: 'devnet',
|
||||
appUrl: 'https://app-dev.hanzo.ai',
|
||||
payUrl: 'https://app-dev.hanzo.ai/pay',
|
||||
} as Parameters<typeof appsOf>[0]
|
||||
assert.deepEqual(appsOf(devnet).map((a) => a.href), ['https://app-dev.hanzo.ai'])
|
||||
assert.equal(billingOf(devnet), 'https://app-dev.hanzo.ai/pay')
|
||||
assert.equal(teamOf(devnet), 'https://app-dev.hanzo.ai/account/organization')
|
||||
|
||||
// Mainnet's hanzo answers from the brand tables, unchanged.
|
||||
const mainnet = { orgId: 'hanzo' } as Parameters<typeof appsOf>[0]
|
||||
assert.deepEqual(appsOf(mainnet), appsFor('hanzo'))
|
||||
assert.equal(billingOf(mainnet), billingFor('hanzo'))
|
||||
assert.equal(teamOf(mainnet), 'https://hanzo.ai/account/organization')
|
||||
})
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
* hostname switches; unknown orgs fall back to `hanzo`.
|
||||
*/
|
||||
|
||||
import type { Org } from '@hanzo/id-shared'
|
||||
|
||||
export interface Quote {
|
||||
readonly text: string
|
||||
readonly author: string
|
||||
@@ -145,3 +147,28 @@ const TEAM: Record<string, string> = {
|
||||
export function teamFor(orgId: string): string | undefined {
|
||||
return TEAM[orgId]
|
||||
}
|
||||
|
||||
/**
|
||||
* A host whose catalog entry names its network's app (`appUrl`: the devnet's
|
||||
* https://app-dev.hanzo.ai) is answered from that entry, never from the brand's
|
||||
* mainnet tables above: its one app, its own pay surface (`payUrl`), and that
|
||||
* app's organization page. Every other host answers from the tables exactly as
|
||||
* before, so no mainnet host changes.
|
||||
*/
|
||||
export function appsOf(org: Org): readonly AppLink[] {
|
||||
const listed = appsFor(org.orgId)
|
||||
if (org.appUrl) return [{ ...listed[0]!, href: org.appUrl }]
|
||||
return listed
|
||||
}
|
||||
|
||||
/** The billing surface for this host: its network's pay surface, or the brand's billing host. */
|
||||
export function billingOf(org: Org): string | undefined {
|
||||
if (org.appUrl) return org.payUrl
|
||||
return billingFor(org.orgId)
|
||||
}
|
||||
|
||||
/** The organization page for this host: on its network's app, or the brand's. */
|
||||
export function teamOf(org: Org): string | undefined {
|
||||
if (org.appUrl) return `${org.appUrl}/account/organization`
|
||||
return teamFor(org.orgId)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useId, useState, type FormEvent } from 'react'
|
||||
import type { Brand, Org } from '@hanzo/id-shared'
|
||||
import { Alert, SignupForm, Submit, type Account, type AuthClient } from '@hanzo/id-auth'
|
||||
import { BrandFooter } from '../components/BrandFooter'
|
||||
import { teamFor } from '../marketing'
|
||||
import { teamOf } from '../marketing'
|
||||
import { clientIdFrom } from '../route'
|
||||
|
||||
/**
|
||||
@@ -72,7 +72,7 @@ export function Join({ client, brand, org }: { client: AuthClient; brand: Brand;
|
||||
const here = `/join?${new URLSearchParams({ ...app, invite: code, org: owner })}`
|
||||
const signin = `/login?${new URLSearchParams({ ...app, return: here })}`
|
||||
// Onward, to where this brand manages an organization; its own portal otherwise.
|
||||
const onward = teamFor(org.orgId) ?? '/'
|
||||
const onward = teamOf(org) ?? '/'
|
||||
|
||||
// One call for every step. Without `withCode` it asks to join, and for a pinned
|
||||
// invitation that is also how a code is sent: IAM sends one to the account's
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { Brand, Org } from '@hanzo/id-shared'
|
||||
import type { AuthClient } from '@hanzo/id-auth'
|
||||
import { Login } from './Login'
|
||||
import { BrandFooter } from '../components/BrandFooter'
|
||||
import { appsFor, billingFor } from '../marketing'
|
||||
import { appsOf, billingOf } from '../marketing'
|
||||
|
||||
type Auth =
|
||||
| { s: 'loading' }
|
||||
@@ -36,7 +36,7 @@ export function Portal({
|
||||
// step; the launcher below renders only for a brand that names no app.
|
||||
// A sign-in bounced from /onboarding (its token_bounce guard is set) goes
|
||||
// back there to finish the plan step.
|
||||
const home = appsFor(org.orgId)[0]?.href
|
||||
const home = appsOf(org)[0]?.href
|
||||
if ((account || justSignedIn) && home) {
|
||||
window.location.replace(sessionStorage.getItem('onboarding.token_bounce') ? '/onboarding' : home)
|
||||
return
|
||||
@@ -86,10 +86,10 @@ export function Portal({
|
||||
if (auth.s === 'anon') return <Login client={client} brand={brand} />
|
||||
|
||||
// Signed in: the apps launcher.
|
||||
const apps = appsFor(org.orgId)
|
||||
const apps = appsOf(org)
|
||||
// undefined for a brand with no billing host — UserMenu renders the row only when
|
||||
// it has a URL, so the tile disappears rather than pointing at a dead hostname.
|
||||
const billingUrl = billingFor(org.orgId)
|
||||
const billingUrl = billingOf(org)
|
||||
// signOut, not logout: the latter only builds the IdP URL and leaves this
|
||||
// browser's `hanzo_iam_*` keys in place, so the token string outlived the
|
||||
// session it named. Called on click, not at render — it clears storage.
|
||||
|
||||
@@ -320,3 +320,65 @@ test('a brand that declares no legal pages resolves none', () => {
|
||||
assert.equal(org.termsUrl, undefined)
|
||||
assert.equal(org.privacyUrl, undefined)
|
||||
})
|
||||
|
||||
// The devnet's identity host, id-dev.hanzo.ai, is org `hanzo` on another
|
||||
// network: its own IAM and its own API. An alias of org hanzo on mainnet goes
|
||||
// to hanzo.id, and a devnet host must never go there, so front doors are read
|
||||
// per network.
|
||||
test('a devnet host is its own network: never sent to mainnet\'s front door', () => {
|
||||
const catalog = parseCatalog(
|
||||
JSON.stringify({
|
||||
'hanzo.id': { orgId: 'hanzo', canonical: true },
|
||||
'iam.hanzo.ai': { orgId: 'hanzo' },
|
||||
'id-dev.hanzo.ai': { orgId: 'hanzo', network: 'devnet' },
|
||||
}),
|
||||
)
|
||||
|
||||
assert.equal(frontDoor(catalog, 'hanzo'), 'https://hanzo.id')
|
||||
assert.equal(frontDoor(catalog, 'hanzo', 'devnet'), '', 'the devnet names no front door, so its host stays put')
|
||||
assert.equal(aliasRedirect(catalog, 'hanzo', 'https://id-dev.hanzo.ai', '/login/oauth/authorize', '?client_id=hanzo-console'), null)
|
||||
assert.equal(aliasRedirect(catalog, 'hanzo', 'https://id-dev.hanzo.ai', '/', ''), null)
|
||||
// Mainnet's alias is unchanged.
|
||||
assert.equal(aliasRedirect(catalog, 'hanzo', 'https://iam.hanzo.ai', '/login', '?a=1'), 'https://hanzo.id/login?a=1')
|
||||
|
||||
// A devnet front door is the devnet's, and mainnet's aliases never reach it.
|
||||
const both = parseCatalog(
|
||||
JSON.stringify({
|
||||
'hanzo.id': { orgId: 'hanzo', canonical: true },
|
||||
'id-dev.hanzo.ai': { orgId: 'hanzo', network: 'devnet', canonical: true },
|
||||
'alias-dev.hanzo.ai': { orgId: 'hanzo', network: 'devnet' },
|
||||
}),
|
||||
)
|
||||
assert.equal(aliasRedirect(both, 'hanzo', 'https://alias-dev.hanzo.ai', '/x', ''), 'https://id-dev.hanzo.ai/x')
|
||||
assert.equal(aliasRedirect(both, 'hanzo', 'https://id-dev.hanzo.ai', '/x', ''), null)
|
||||
assert.equal(frontDoor(both, 'hanzo'), 'https://hanzo.id')
|
||||
})
|
||||
|
||||
// The catalog this export ships (apps/web/public/config.json) is what id-dev
|
||||
// reads. Asserted through the reader: every URL it resolves to is the devnet's
|
||||
// own, and nothing of mainnet's (hanzo.id, api.hanzo.ai, the apex) is named.
|
||||
test('the shipped catalog keeps id-dev on the devnet', async () => {
|
||||
const { readFile } = await import('node:fs/promises')
|
||||
const { join } = await import('node:path')
|
||||
const payload = JSON.parse(await readFile(join(import.meta.dirname, '../../../apps/web/public/config.json'), 'utf8'))
|
||||
const catalog = parseCatalog(catalogOf(payload))
|
||||
const org = resolveOrg('id-dev.hanzo.ai', { catalog })
|
||||
|
||||
assert.equal(org.orgId, 'hanzo')
|
||||
assert.equal(org.network, 'devnet')
|
||||
assert.equal(org.iamUrl, 'https://id-dev.hanzo.ai')
|
||||
assert.equal(org.iamIssuer, 'https://id-dev.hanzo.ai')
|
||||
assert.equal(org.publicOrigin, 'https://id-dev.hanzo.ai')
|
||||
assert.equal(org.appUrl, 'https://app-dev.hanzo.ai')
|
||||
assert.equal(org.apiUrl, 'https://api-dev.hanzo.ai')
|
||||
assert.equal(org.payUrl, 'https://app-dev.hanzo.ai/pay')
|
||||
assert.equal(org.brandPackage, '@hanzo/brand')
|
||||
for (const url of [org.iamUrl, org.iamIssuer, org.publicOrigin, org.appUrl, org.apiUrl, org.payUrl]) {
|
||||
assert.match(new URL(url!).hostname, /^[a-z0-9]+-dev\.hanzo\.ai$/, `${url} is a devnet host`)
|
||||
}
|
||||
assert.equal(aliasRedirect(catalog, org.orgId, 'https://id-dev.hanzo.ai', '/login', ''), null)
|
||||
// Mainnet's hosts resolve as they did.
|
||||
assert.equal(resolveOrg('hanzo.id', { catalog }).iamIssuer, 'https://hanzo.id')
|
||||
assert.equal(resolveOrg('hanzo.id', { catalog }).network, undefined)
|
||||
assert.equal(aliasRedirect(catalog, 'hanzo', 'https://iam.hanzo.ai', '/login', ''), 'https://hanzo.id/login')
|
||||
})
|
||||
|
||||
+19
-3
@@ -197,6 +197,9 @@ function fromCatalog(entry: CatalogEntry | undefined): Partial<Org> {
|
||||
'publicOrigin',
|
||||
'brandPackage',
|
||||
'payUrl',
|
||||
'network',
|
||||
'appUrl',
|
||||
'apiUrl',
|
||||
'termsUrl',
|
||||
'privacyUrl',
|
||||
] as const) {
|
||||
@@ -243,10 +246,10 @@ function stripPort(h: string): string {
|
||||
* Returns '' when the org names no front door — local dev, an unregistered host,
|
||||
* or `bootnode`, whose only host IS its front door. '' means "stay here".
|
||||
*/
|
||||
export function frontDoor(catalog: Catalog, orgId: string): string {
|
||||
export function frontDoor(catalog: Catalog, orgId: string, network = ''): string {
|
||||
if (!orgId) return ''
|
||||
for (const [host, e] of Object.entries(catalog)) {
|
||||
if (e?.canonical && e.orgId === orgId) return `https://${stripPort(host)}`
|
||||
if (e?.canonical && e.orgId === orgId && (e.network ?? '') === network) return `https://${stripPort(host)}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -273,11 +276,24 @@ export function aliasRedirect(
|
||||
search: string,
|
||||
): string | null {
|
||||
if (pathname === '/callback' || pathname.startsWith('/callback/')) return null
|
||||
const door = frontDoor(catalog, orgId)
|
||||
// The front door is the one on THIS host's network: a devnet host is never
|
||||
// sent to a mainnet one, and an org with none on its network stays put.
|
||||
const door = frontDoor(catalog, orgId, networkOf(catalog, origin))
|
||||
if (!door || door === TRIM_TRAILING_SLASH(origin)) return null
|
||||
return `${door}${pathname}${search}`
|
||||
}
|
||||
|
||||
/** The network the catalog states for the host of `origin`; '' is mainnet. */
|
||||
function networkOf(catalog: Catalog, origin: string): string {
|
||||
let host = ''
|
||||
try {
|
||||
host = new URL(origin).hostname.toLowerCase()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
return catalog[host]?.network ?? ''
|
||||
}
|
||||
|
||||
function normalize(t: Org): Org {
|
||||
const publicOrigin = TRIM_TRAILING_SLASH(t.publicOrigin)
|
||||
const iamIssuer = TRIM_TRAILING_SLASH(t.iamIssuer || t.iamUrl)
|
||||
|
||||
@@ -40,9 +40,10 @@ export interface Org {
|
||||
/**
|
||||
* This host is the org's front door — the one its people sign in on.
|
||||
*
|
||||
* At most one entry per org sets it. Every other host for that org is an
|
||||
* alias and redirects here, carrying its path and query (see `aliasRedirect`).
|
||||
* An org that sets it nowhere has no front door and every host stays put,
|
||||
* At most one entry per org per `network` sets it. Every other host for
|
||||
* that org on that network is an alias and redirects here, carrying its path
|
||||
* and query (see `aliasRedirect`). An org that sets it nowhere on a network
|
||||
* has no front door there, and every host on it stays put,
|
||||
* which is the right answer for a single-host brand and for local dev.
|
||||
*
|
||||
* It is a fact about the ORG stated in a table keyed by HOST, which is why it
|
||||
@@ -51,6 +52,24 @@ export interface Org {
|
||||
* thing to keep in step.
|
||||
*/
|
||||
readonly canonical?: boolean
|
||||
/**
|
||||
* The network this host serves: absent is mainnet, `devnet` the dev network
|
||||
* (its own IAM, its own API, sandbox money). An org has a front door per
|
||||
* network, so an alias is sent to its own network's front door and a devnet
|
||||
* host is never sent to a mainnet one.
|
||||
*/
|
||||
readonly network?: string
|
||||
/**
|
||||
* The brand's main app on this host's network, where a signed-in person is
|
||||
* sent and the one app the launcher lists. Absent, the brand's product list
|
||||
* (`appsFor`) answers. NO trailing slash.
|
||||
*/
|
||||
readonly appUrl?: string
|
||||
/**
|
||||
* The gateway this host reports its telemetry to. Absent, api.hanzo.ai.
|
||||
* NO trailing slash.
|
||||
*/
|
||||
readonly apiUrl?: string
|
||||
/** npm package name of the brand pkg to load (e.g. `@hanzo/brand`). */
|
||||
readonly brandPackage: string
|
||||
/** Optional absolute URL to brand.json (e.g. a jsDelivr-hosted copy from
|
||||
|
||||
Reference in New Issue
Block a user