493 Commits
Author SHA1 Message Date
zooqueenandHanzo Dev 6e6332304d Counting a sign-up never blocks it, and reaches the client that is live when it happens
Every telemetry step on the sign-up path runs inside one guard that logs and
passes over a throw, so a full or refused browser store no longer stops the
account from being created or the browser from leaving for the app. Each step
reads the client when it happens rather than when the form rendered, so a
visitor who accepts and submits at once still has the join and the completion
counted once the live client arrives.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 11:37:43 -07:00
zooqueenandHanzo Dev c98ca92754 Merge main: a devnet identity host's policy and stream go to its own gateway
Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 11:33:11 -07:00
zooqueenandHanzo Dev 0814531906 Refuse any publishable key, and @hanzo/event's key lookups, in shipped source
The source this image ships — the app and workspace packages, tests and specs
aside, public/ beside the keyring, index.html, vite.config.ts and any .env —
may name no pk- key at all, a placeholder of x aside, and the app and packages
may not name ORG_KEY, SITE_KEY, keyFor, keyForPage or siteKey. The account
page's own storage-key constant is CURRENT_ORG, so the name stays the library's.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 11:09:56 -07:00
zooqueenandHanzo Dev 7eb7b28d40 Load no browser tag on identity hosts, and start the stream only under the policy in force
The portal no longer starts @hanzo/event's tag manager. Once a stored or new
choice allows Analytics, it asks cloud for the brand's consent policy with the
brand's key and no credential, hands only the policy to @hanzo/event, and
leaves the tags cloud lists beside it unloaded. The event client starts after
the policy arrives and only if the choice still counts under it, so an Accept
stored under an older policy, a refusal cloud saw, or no answer at all starts
nothing and stores nothing, and the banner asks again.

The page reloads only when the visitor withdraws Analytics after the client
started, on the choice they store, never on a policy's arrival. The completed
sign-up goes to the stream with the visitor's consent stamped on it, for cloud
to forward server-side.

The spec lists Google's and Meta's tags for every key and holds every identity
host to loading none, and covers a newer policy, a refusal cloud saw and GPC
over a stored Accept: one load, the form, nothing sent or kept.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 11:09:56 -07:00
zooqueenandHanzo Dev 0a0f7c8f51 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]>
2026-10-16 07:27:09 -07:00
zooqueenandHanzo Dev 6c4b180d4e Keep the sign-in standing and silent whatever the banner or the visitor does
The banner's chunk is optional: one that does not load, or throws as it
renders, draws nothing and the sign-in form stays. A withdrawal of Analytics
after the event client started reloads the page once, so no client is left to
send. Every identity page sends only its origin as a referrer, so an address
holding a code, an invite or an OIDC request reaches no other page.

The consent spec answers tag config as production does: each identity host's
key names a project with no tags, so after Accept no Google or Meta request
leaves the page and the sign-up reaches the stream with its consent. It
records every request that leaves the host and holds it to what the page may
send at that moment, throws errors before and after a choice, and covers a
failed chunk, Reject after Accept, and the referrer.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 07:09:28 -07:00
zooqueenandHanzo Dev 7505a7b8bc A tag that throws at sign-up never keeps the new account on the page
The account exists before its completion is stated, so track() runs inside a
try: a throwing pixel is logged, and the browser still leaves for the app that
asked.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 07:09:27 -07:00
zooqueenandHanzo Dev d6d6e203f6 Refuse a publishable key the source names and the runtime keyring does not
The dist check allows @hanzo/event's host-keyed table, so a key copied from
that table into the app's own code would pass it while naming one brand on
every host. The shipped source (the app and workspace packages, tests aside,
public/, index.html and vite.config.ts) may now name only keys the runtime
keyring in public/config.json names; any other fails the image build.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 07:09:27 -07:00
zooqueenandHanzo Dev 9c5268d8a2 Count a completed sign-up once, on the stream and the brand's tags, through track()
The account's creation is one track(stream, 'signup_completed') under one
event id: our stream hears it, and so does each of the brand's tags the
visitor's choice allows, as GA4 sign_up and Meta CompleteRegistration. The
page address track() stamps into the moment's properties leaves as its path,
like the event's own location fields.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 02:42:30 -07:00
zooqueenandHanzo Dev 09f54bd496 Ask before measuring on every identity host, and load each brand's own tags
index.html drops its hard-coded GA4, Meta Pixel, Umami and Insights tags. The
shared @hanzo/ui banner asks and @hanzo/event keeps the choice: until it allows
Analytics no event client exists, and until it allows Analytics or Marketing
the tag manager is not started, so nothing is fetched, sent or stored. Global
Privacy Control and Do Not Track refuse without being asked.

The tag manager asks cloud for the tag set of the brand's runtime key, the same
key its event stream files under, so each brand loads only its own GA4 and
pixels and a host with no key starts none. A host whose brand publishes no
privacy policy is not asked, so nothing optional runs there. Routes whose
address carries an authorization code or device code start no tags either.

@hanzo/event 0.3.58 and @hanzo/ui 8.27.90 are pinned, with one @hanzo/event in
the graph. The banner renders on @hanzo/gui through react-native-web and loads
on its own chunk. The dist key check reads its allowance from the runtime
keyring and from @hanzo/event's host-keyed table.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 02:41:50 -07:00
zooqueenandHanzo Dev 9f22a78da6 A sign-in IAM holds for an unproven address stops on Confirm your email, on every way in
IAM answers a sign-in of an account whose address was never proven with
RequiredEmail and the address it sent a 6-digit code to ("" when none is on
file). The client reads it as a third held state beside NextMfa and
RequiredMfa, carrying the sign-in's own OAuth parameters and never its
credential; emailConfirm and emailSend post them again to /v1/iam/login with
{passcode} or {email}, riding the challenge cookie.

EmailConfirmForm is the step: the code, a new code, a different address, and
the address field first when none is on file. The login, device-approval and
signup pages render it for password, code and wallet sign-ins. A confirmed
address continues as the sign-in would have, second factor included, and an
expired hold returns to the sign-in form with IAM's sentence.

Signup shows IAM's refusals for an address it cannot verify, or for no address,
like any other refusal; the note that such an account is made unproven is gone.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-14 14:16:31 -07:00
zooqueenandHanzo Dev 0fe3499290 A sign-in bounced from onboarding returns to onboarding; every other signed-in visit goes to the brand's main app.
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-30 15:02:03 -07:00
zooqueenandHanzo Dev 10536d2de8 A signed-in visit to hanzo.id goes straight to the brand's main app instead of the apps launcher.
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-30 14:58:44 -07:00
z e4bb19f6b9 Grant hanzoai/ci the run's ID token [skip ci] 2026-09-30 07:17:36 -07:00
zeekay be5d5b5504 Take @hanzogui/shell 8.3.20: account and sign-in links stay on this site 2026-09-30 02:38:23 -07:00
zooqueenandHanzo Dev b993000391 feat(pricing): update onboarding pro plan price to $20/mo (2000 cents)
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-30 00:35:16 -07:00
zooqueenandHanzo Dev 09717d0cad feat(analytics): add GA4 (G-EKBNQ969GE), Meta Pixel, Hanzo Analytics, and Hanzo Insights
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-30 00:35:16 -07:00
zeekay 170454b9fc web: no source maps in the production build 2026-09-29 21:53:05 -07:00
zooqueenandHanzo Dev c667b911fc feat(auth): prompt plan payment immediately after login and credential entry
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-29 21:01:31 -07:00
zooqueenandHanzo Dev fe1c30e84f test(e2e): assert plan prompt cards in login e2e spec
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-29 20:50:34 -07:00
zooqueenandHanzo Dev 4027667d37 feat(plan): prompt plan immediately on hanzo.id and onboarding flow
- Display plan selector (Pro, Team, Pay as you go) directly on sign-in when accessed without an external redirect_uri
- Route bare sign-ins straight to checkout (pay.hanzo.ai) to complete plan payment
- Move plan step to Step 1 in onboarding flow (STEPS = ['plan', 'org', 'project', 'wallet', 'consent'])
- Redirect uncompleted/unpaid accounts on Portal.tsx immediately to /onboarding
- Enforce reachability so no subsequent steps can be visited before plan is selected

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-29 20:30:57 -07:00
zooqueenandHanzo Dev 61edd784de White-label sign-in pages name their own brand in the SMS consent and carry no Hanzo class names, comments or brand data.
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-29 16:01:03 -07:00
zeekay 5755e9c9df Registration starts with the address the app sent
An app that takes a work email on its own page — hanzo.team's Get
Team — sends it as login_hint with screen_hint=signup. Sign-in already
read the hint; registration dropped it, so the person typed the address
twice. SignupForm takes an initial email and /signup passes the hint.

Claude-Session: https://claude.ai/code/session_01HuMDfgFssPUptYUzgZ6Cp8
2026-09-27 14:40:31 -07:00
zooqueenandHanzo Dev aa54c2cc82 A pinned invite's code comes from accept itself; /join shows IAM's sentence and asks again for a new one
Accept without emailCode sends the code to the signed-in account and answers email_code_sent (or email_code_required inside the resend window); its msg becomes the code step's line. Send a new code is accept again; submit is accept with emailCode. The page no longer requests a code or looks up the application for one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-27 09:12:43 -07:00
zooqueenandHanzo Dev f47471cdef A pinned invite asks the signed-in account for a code sent to its own address, then joins with it
acceptInvitation sends emailCode and reports IAM's reason code. On email_code_required /join sends a code to the account's email under the link's application and accepts again with it; a wrong code, a different address and too many attempts show IAM's sentence as written.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-27 09:04:52 -07:00
zooqueenandHanzo Dev fe93d53eae /join goes through the app its link names, and reads IAM's accept envelope
Invite links carry client_id; registration and the sign-in that comes back both go through that app, and the host's own when the link names none. acceptInvitation reports a join only for {status:ok, data:{joined:true}} and shows IAM's refusal sentence as written.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-26 13:59:11 -07:00
zooqueenandHanzo Dev d9a6eed933 An invite link opens /join: an account made on the invitation, or a sign-in that comes back to join
Signed out, /join?org=&invite= offers registration that carries organization and invitationCode to IAM, or a sign-in whose ?return= (same-origin /join only) brings the person back. Signed in, Join posts {owner, code} to /v1/iam/invitations/accept. An account created on the invitation is shown as already in the org.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-26 13:32:11 -07:00
zooqueenandHanzo Dev f61bdf589d Registration says an address already has an account before any code, and offers sign-in and a reset
The first submit carries no code, so IAM answers a taken address or a refused password there. A taken address shows Sign in and Reset password, both opening on that address for the same request. Recovery prefills from login_hint and returns to the same sign-in.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-26 10:50:58 -07:00
zooqueenandHanzo Dev b82b25fe9f The corner mark keeps a row of its own
A page as tall as the viewport pushed its heading up into the mark's
row: "Hanzo ID" at 19px semibold beside "Sign in" at 17px bold, a
pixel or two apart, a header that reads as almost aligned. The page
now starts its column under the mark's 28px row plus 16px, so the
heading sits below the mark at every width. The e2e asserts it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-24 16:49:58 -07:00
zooqueenandHanzo Dev e2ad0c1529 Sign in reads "No account? Create a new account"
The owner's wording for the line under the sign-in column. Same link,
same gate on enableSignUp, same request carried to /signup.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-24 16:20:00 -07:00
zooqueenandHanzo Dev f5a86d7351 Sign in offers Create account, and registration links back
The sign-in page closes on "Don't have an account? Create account" when the
application takes new accounts (enableSignUp on its row, read from
/v1/iam/auth/application). An unreadable answer draws nothing; the server
stays the only gate.

route.ts builds both links. signupHref carries the query verbatim and keeps
the plain-link shape (/login/<app> -> /signup/<app>), so client_id,
redirect_uri, state, nonce and the PKCE challenge reach registration and the
new account returns to the app that sent it. signinHref is its inverse and
drops only signup=true / screen_hint=signup, which /login would answer by
forwarding straight back. The signup page's "Sign in" links and the
post-signup MFA hand-off use it.

Unit tests pin every parameter and both shapes; the e2e now asserts the
control, the round trip and its absence when signup is off.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-24 11:26:25 -07:00
zeekayandClaude Opus 5.5 2d98144463 pars.id draws the Pars mark: take @parsdao/brand 1.0.3
Hanzo CI/CD / test (linux-amd64) (push) Successful in 3m29s
Hanzo CI/CD / test (linux-arm64) (push) Successful in 8m26s
Hanzo CI/CD / cicd (push) Successful in 4m17s
CI/CD / cicd (push) Successful in 27m0s
1.0.1's brand.json named its logo and favicon at @latest, and that release
ships no assets, so pars.id and id.pars.network drew a 404 for both. 1.0.3
carries them and names itself. It is a day old, so it is excluded from pnpm's
release-age hold.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-23 20:09:58 -07:00
zeekay ea07fa8c47 Ship the osage brand with the site
Hanzo CI/CD / test (linux-amd64) (push) Successful in 2m24s
Hanzo CI/CD / test (linux-arm64) (push) Successful in 8m12s
Hanzo CI/CD / cicd (push) Successful in 2m15s
CI/CD / cicd (push) Successful in 10m31s
osage.id is served by this bundle, but osage has no brand package for
the build to resolve, so its brand JSON and mark were placed in the
bucket by hand and every publish of a fresh build dropped them. In
public/ they are part of the build like any other file.

Claude-Session: https://claude.ai/code/session_01HuMDfgFssPUptYUzgZ6Cp8
2026-09-23 18:00:00 -07:00
zeekayandHanzo Dev 405f18651a The site publishes as hanzo-id, a slug the Sites plane accepts
Hanzo CI/CD / test (linux-amd64) (push) Successful in 2m54s
Hanzo CI/CD / test (linux-arm64) (push) Successful in 9m21s
Hanzo CI/CD / cicd (push) Successful in 2m4s
CI/CD / cicd (push) Successful in 26m52s
`id` is a reserved label (cloud apps/sites/reserved.go), so the site lane's
POST /v1/projects answered 400 "slug is a reserved subdomain" and run 12348
stopped at Publish site with every gate green. hanzo-id is hanzo.id's own
name, as hanzo-team is hanzo.team's; the edge root follows it in universe.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 17:15:24 -07:00
zeekayandHanzo Dev f329fbb931 The e2e gate installs Chromium without --with-deps
Hanzo CI/CD / test (linux-amd64) (push) Successful in 2m14s
Hanzo CI/CD / test (linux-arm64) (push) Successful in 7m52s
Hanzo CI/CD / cicd (push) Failing after 2m26s
CI/CD / cicd (push) Failing after 10m25s
The forge runner image (hanzoai/ci runner/Dockerfile) already carries
Chromium's shared libraries, and its sandbox refuses sudo, so
`playwright install --with-deps` stopped run 12345 at the apt step before a
single spec ran. The gate now fetches only the browser that matches
@playwright/test into $RUNNER_TEMP, as hanzo.ai's gate does.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 16:58:24 -07:00
zeekayandHanzo Dev d2bfa1d46a Sign in opens on the email field, with every other way in under it
The page now reads, on every brand: Sign in, Email or username, Password,
Continue, Send me a code instead, then "or", then Google, GitHub, Wallet and
Phone, each drawn only when the application can complete it, then Forgot
password? and the footer. PROVIDER_ORDER puts `form` first and the rule
follows the form, so login, signup and device approval share the one column.

The "New here?" / "Create account" block is gone, along with the sentences
that pointed at it (the refusal note and the unlinked-wallet message). /signup
stays: hanzo.app's signup=true and hanzo.chat's /signup/<client> still land
there. Server sign-up rules are unchanged.

apps/web/e2e drives the built export in Chromium on hanzo.id and lux.id at
1440 and 390 and asserts that order and the absence of any registration
control; hanzo.yml runs it as a fourth gate.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 16:28:40 -07:00
zooqueenandHanzo Dev 1261a5ab88 Onboarding completes when checkout returns paid
The plan step records the choice and sends the buyer to checkout with the
org they founded as ?org=, so pay charges that org's ledger rather than the
signup org. Completion records only when pay returns to /onboarding with
checkout=success; leaving checkout unpaid reopens the plan step. Plans show
the monthly price checkout sells, and a per-seat plan states its seat
minimum and first charge from the catalog's perSeat and limits.minSeats.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 13:17:34 -07:00
zooqueenandHanzo Dev 80929e5a67 Onboarding ends at checkout: the plan step has no Skip
Choosing a plan or a prepaid balance is the only way out of onboarding; it
records completion and sends the person to checkout. The step machine refuses
an empty answer on a step that is not skippable, so Skip, the arrow keys and
the step dots cannot reach done without a plan. A catalog that fails to load
offers Retry rather than a choice whose checkout cannot answer.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 12:48:47 -07:00
zeekayandHanzo Dev eea9c655c6 A new account's address is proven before it exists
The signup screen created the account from an address nobody had proven, and
nothing a password account can reach proves it later, so IAM kept the address
unproven for good. Cloud refuses the git path to an unproven address, and the
federation broker will not link a social identity onto one.

Where the application can deliver a code, the screen now sends one to the
address first and creates the account with it, and IAM records the address
proven. A refused send is shown and creates nothing. Changing the address sends
the new one its own code. An application that cannot deliver a code registers
as before.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 05:48:41 -07:00
zeekayandHanzo Dev c031f3d2b9 The identity portal publishes as a site
Every id host renders the same SPA, and the only thing the pod adds is
/config.json, which it writes at boot from two SPA_* env values: the per-host
org catalog and the ingest keyring, both public (the browser reads them). That
file now ships in the export, and the site lane publishes it to
s3://hanzo-sites/hanzo/id, so the edge can serve hanzo.id and its brand hosts
with no pod. The image keeps its SPA_* env until the pod is retired, and that
env still wins inside the pod.

Checked in headless Chromium with /v1 and /.well-known left to cloud: the export
renders what the pods render on hanzo.id, iam.hanzo.ai, lux.id, id.lux.network,
zoolabs.id and pars.id.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 10:17:21 -07:00
zeekayandHanzo Dev 40b19fde3e release 1.1.4
Co-authored-by: Hanzo Dev <[email protected]>
v1.1.4
2026-09-19 04:24:47 -07:00
zeekayandHanzo Dev 1a027428bb Measure what the client still ships without the door
Two tests exist to prove why the route gate and bare() exist: the same view,
sent by an unmodified client, put the OAuth code, state, challenge and nonce
on the wire. They asserted the leak by those values, and since @hanzo/event
0.3.13 the client redacts a query value by the NAME it is filed under —
code, state, nonce, token and a list more — so those values no longer leave
and both tests failed on their own, in any order. The suites were written
against 0.3.12.

The gate and bare() are still load-bearing, and the tests now measure why.
The client's list is a list. Measured against 0.3.41: an ungated pageview
from /login/oauth/device?user_code=… ships the device code while path reads
clean, and an undoored signup_viewed from /signup?… ships client_id and
code_challenge in both url and referrer while state and nonce leave as
[redacted]. No list names every parameter an identity flow will carry, so
the page's rule stays the route and the whole query, not a name.

The comments in analytics.tsx said the client redacts secret shapes only.
That was 0.3.12. They now say what it does.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 03:35:02 -07:00
zeekayandHanzo Dev 0119162b78 Start each test document with no event clients
hanzo.id appeared to report on Lux's key. The page does not; the suite did.

Since @hanzo/event 0.3.27 createAnalytics returns the page's client for a
(host, product) stream, and a later caller is handed the first caller's key,
transport and identity — adopt() never replaces a key that is present. In a
browser that is the point: a page is one document on one host, so the first
key is the right one. A test file is one realm for all of its tests, and
these visit lux.id and then hanzo.id in it, so every document after the
first was handed the first one's client. The suites were written against
0.3.12, which built a client per call; 4043d05 moved the lockfile to 0.3.37
and the suites did not move with it. Four failures, one cause:

  - a Hanzo host reported on Lux's key: the previous test's client;
  - a client built with its own transport received nothing: same;
  - an arrival was already the person: the previous test's identify();
  - the second location in one test reported nothing: the first one's
    transport.

Each passes alone and fails after another. newDocument() drops what a page
keeps for its lifetime — the /config.json memo and the clients — through the
reset the package names, and every suite that visits documents begins each
one with it.

Without resetClients() in newDocument() all four fail again; with it the
three suites pass in five shuffled orders. The two tests in these files that
still fail measure something else — next commit.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 03:35:02 -07:00
zeekayandHanzo Dev fde7538443 Take @hanzo/event 0.3.41, which names the page-client reset
Since 0.3.27 createAnalytics keeps one client per (host, product) on the
page, under a Symbol.for slot, and hands every later caller that client. A
suite that visits several documents in one file has to drop it between them,
and 0.3.37 gave that no name, so a suite had to delete the slot itself.
0.3.40 exports resetClients() for exactly this; 0.3.41 is the current release.

The source diff from 0.3.37 is confined to that export, `zoolabs.io` in the
package's own domain table (unused here: the live key comes from the runtime
keyring, never from the package) and VERSION read from package.json instead
of restated. The react peer range moved to >=19.2.8, which the catalog pins.

The suites take it up in the next commit.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 03:34:34 -07:00
zeekayandHanzo Dev d734e5a1c7 Restart a sign-in this page cannot complete
Arriving with a state this origin holds no transaction for is ordinary, not
exceptional: an old link carrying ?code=&state= is followed twice, the attempt
began in another profile or a private window, or site data was cleared between
the two halves. @hanzo/iam refuses it and is right to — that check is the CSRF
protection, and its own test forbids soft-skipping it.

The answer was wrong, though. This page caught every failure alike and printed
the SDK's sentence, so a recoverable return became a dead end whose only
instruction — restart sign-in — was the one thing it gave no way to do. The
query is what cannot be completed, so it is dropped and the portal's login page
begins a fresh transaction.

Once. A restart that cannot succeed must not become a loop, so the attempt is
marked and a second arrival shows the error and stops; a sign-in that genuinely
failed is reported and never bounced. post_login_redirect is left alone, since
where the person was going is still true.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 16:43:52 -07:00
zooqueenandHanzo Dev 94590d7acc fix(ci): allow upstream @hanzo/event fallback keys in dist assertion
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-15 14:52:24 -07:00
zooqueenandHanzo Dev e0ee4eb6c6 fix(id-web): declare noble and bs58 workspace dependencies in apps/web
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-15 14:47:34 -07:00
zooqueenandHanzo Dev 7d8451e011 feat(id): update launcher app list with AI, Agency, Team, Bot, App, Cloud, Chat, Pay
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-15 12:24:17 -07:00
zooqueenandHanzo Dev c48c1e4901 Offer the accounts signed in on this browser when asked to choose
With prompt=select_account the sign-in page lists the people IAM holds a
session for (GET /v1/iam/accounts) and a "Use another account" row that
opens the credential form. Choosing a person re-enters authorize with the
request the page was handed plus login_hint, and IAM answers from that
person's session. Nobody signed in here goes straight to the form.

login_hint also starts the credential form with that identifier.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-15 06:08:46 -07:00
zooqueenandHanzo Dev b353c6f17b Import the design token layer the portal is written against
@hanzo/font is not a dependency of this workspace, so vite build stops
at `@import '@hanzo/font/css'`. @hanzo/design/styles.css is the file the
comment above the import describes: every token group plus the Zen faces
the package ships. With it the build passes and tokens.test.ts finds
every referenced token defined.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-15 06:07:28 -07:00