mirror of
https://github.com/hanzoai/id.git
synced 2026-10-16 18:38:15 +00:00
hanzo.id is ONE app, not whichever won the race
Reported from a phone: fill in email + password on hanzo.id/signup, press
Create account, get "the application does not allow to sign up new account".
Not reproducible on demand, which is the tell.
resolveOrg spreads the runtime catalog OVER the built-in table, and for
hanzo.id the two named DIFFERENT applications:
built-in DEFAULT_TENANTS hanzo-id enableSignUp false
catalog /config.json hanzo-console enableSignUp true
So the answer to "can this person create an account" depended on whether a
second network fetch arrived before the form was submitted. When it did, signup
worked -- which is why it looks fine from a desk. When it did not, the page
still rendered a full form (the fallback app permits password login, just not
registration) and IAM refused the POST. On a phone a dropped request is
ordinary, so that is where it showed up.
Confirmed against prod by replaying the exact POST the SPA makes:
clientId=hanzo-console application=hanzo-console -> past the gate
clientId=hanzo-console application=hanzo-id -> "the application does
not allow to sign up
new account"
The second line is the customer's screenshot, reproduced.
lux.id had the same divergence (lux-id vs lux-cloud); both are false there, so
it was latent rather than live. Fixed with hanzo's -- the invariant is one host
one app, not "the currently harmful ones".
The fix is NOT to make the fallback survivable. This table is a fallback for a
failed /config.json, not a second opinion, so the answer is that there is only
one answer per host. org.test.ts now asserts that resolving a host WITH and
WITHOUT the catalog yields the same clientId; reverting org.ts reproduces the
failure ("hanzo.id fallback", 1 failed | 15 passed) and restoring it passes,
so the guard is known to bite. 171/171, typecheck clean.
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -249,3 +249,36 @@ test('catalogOf is reachable from the package barrel', async () => {
|
||||
assert.equal(typeof barrel.catalogOf, 'function')
|
||||
assert.equal(barrel.catalogOf({ iamTenantConfigJson: '{}' }), '{}')
|
||||
})
|
||||
|
||||
// The fallback and the catalog must name the SAME application per host.
|
||||
//
|
||||
// resolveOrg spreads the catalog OVER the built-in table, so a host where the
|
||||
// two disagree authenticates as one app or the other depending on whether
|
||||
// /config.json won a race. hanzo.id said `hanzo-id` in the table and
|
||||
// `hanzo-console` in the catalog, and those two differ in enableSignUp — false
|
||||
// vs true. On a load where the fetch did not arrive, the signup form rendered
|
||||
// and the POST came back "the application does not allow to sign up new
|
||||
// account". Intermittent, device-dependent, and reported from a phone.
|
||||
//
|
||||
// This asserts the property that makes the race harmless: for a host present in
|
||||
// BOTH, resolving with and without the catalog yields the same clientId. It is
|
||||
// checked against the catalog fixture above, which mirrors the ConfigMap in
|
||||
// universe/infra/k8s/id/configmap.yaml — so a change to one side that is not
|
||||
// mirrored in the other fails here rather than in a customer's browser.
|
||||
test('the built-in fallback names the SAME app as the catalog, per host', () => {
|
||||
const CANONICAL: Record<string, string> = {
|
||||
'hanzo.id': 'hanzo-console',
|
||||
'lux.id': 'lux-cloud',
|
||||
'pars.id': 'pars-console',
|
||||
}
|
||||
for (const [host, clientId] of Object.entries(CANONICAL)) {
|
||||
// No catalog — the failed-/config.json path a real visitor can hit.
|
||||
assert.equal(resolveOrg(host).clientId, clientId, `${host} fallback`)
|
||||
// With the catalog — the ordinary path.
|
||||
assert.equal(
|
||||
resolveOrg(host, { catalog: { [host]: { clientId, appName: clientId } } }).clientId,
|
||||
clientId,
|
||||
`${host} with catalog`,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
+22
-8
@@ -31,18 +31,32 @@ const TRIM_TRAILING_SLASH = (s: string): string => s.replace(/\/+$/, '')
|
||||
* backend org-scopes on the `organization` body param; one backend
|
||||
* serves every brand behind its own issuer host.
|
||||
*
|
||||
* `clientId` is the brand `-id` app registered in `init_data.json`
|
||||
* (`hanzo-id`, `lux-id`, …) so the portal authenticates as that app — the
|
||||
* same app whose enabled providers (password + GitHub + Google + Web3)
|
||||
* `get-app-login` reports.
|
||||
* `clientId` MUST name the SAME application the runtime catalog names for that
|
||||
* host. This table is a fallback for a failed `/config.json`, not a second
|
||||
* opinion — `resolveOrg` spreads the catalog OVER it, so any key where the two
|
||||
* disagree makes the host authenticate as one app or the other depending on
|
||||
* whether a network fetch won a race.
|
||||
*
|
||||
* That is not hypothetical. `hanzo.id` said `hanzo-id` here while the catalog
|
||||
* says `hanzo-console`, and the two differ in `enableSignUp` — false vs true.
|
||||
* So on a load where `/config.json` did not arrive, hanzo.id authenticated as
|
||||
* `hanzo-id`, the signup form rendered, and the POST came back "the application
|
||||
* does not allow to sign up new account". Intermittent, device-dependent, and
|
||||
* indistinguishable from a bad password to the person hitting it. Reported from
|
||||
* a phone, where a dropped request is ordinary.
|
||||
*
|
||||
* Both now match the catalog (`universe/infra/k8s/id/configmap.yaml`,
|
||||
* SPA_IAM_TENANT_CONFIG_JSON). `org.test.ts` pins them, so a future edit to one
|
||||
* side has to confront the other. Fixing the fallback to be SURVIVABLE was the
|
||||
* wrong shape — the fix is that there is only one answer per host.
|
||||
*/
|
||||
const DEFAULT_TENANTS: Record<string, OrgConfig> = {
|
||||
'hanzo.id': {
|
||||
orgId: 'hanzo',
|
||||
iamUrl: 'https://hanzo.id',
|
||||
iamIssuer: 'https://hanzo.id',
|
||||
clientId: 'hanzo-id',
|
||||
appName: 'hanzo-id',
|
||||
clientId: 'hanzo-console',
|
||||
appName: 'hanzo-console',
|
||||
publicOrigin: 'https://hanzo.id',
|
||||
brandPackage: '@hanzo/brand',
|
||||
},
|
||||
@@ -50,8 +64,8 @@ const DEFAULT_TENANTS: Record<string, OrgConfig> = {
|
||||
orgId: 'lux',
|
||||
iamUrl: 'https://lux.id',
|
||||
iamIssuer: 'https://lux.id',
|
||||
clientId: 'lux-id',
|
||||
appName: 'lux-id',
|
||||
clientId: 'lux-cloud',
|
||||
appName: 'lux-cloud',
|
||||
publicOrigin: 'https://lux.id',
|
||||
brandPackage: '@luxfi/brand',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user