Commit Graph
481 Commits
Author SHA1 Message Date
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
zooqueenandHanzo Dev 5c1b3a4dcb A wallet with no account attaches to the account you sign in with
Pressing a wallet on the sign-in page sent IAM a login, and IAM's answer
for a wallet nobody holds — "no account is linked to this wallet" — was
drawn as a dead end beside a Create account button. Somebody who already
had an account by email or a provider made a second one to get in.

IAM attaches a verified wallet to an existing identity only when the
verify arrives with that identity's own same-site session (hanzoai/iam
internal/wallet/verify.go). So the refusal is now named (`unlinked`),
the chain is parked in session storage, the page says to sign in below,
and after any sign-in — the form, a second factor, a provider's return
through /callback — the wallet flow runs once more with the session and
IAM links it. From then on the wallet alone signs them in. A declined
second signature leaves the sign-in as it was, wallet unattached.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 21:37:09 -07:00
zeekayandHanzo Dev 70df6265f2 Drop the sync workflow that never ran
It has produced no run on the forge or on GitHub since it was added.
A workflow file that never fires still evaluates on every replayed
ref, so it is a hazard rather than a lane.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-09 00:32:31 -07:00
zeekayandHanzo Dev bcdc844e31 Take the spa base that ships arm64
ghcr.io/hanzoai/spa served only linux/amd64 through 1.4.12, so an image
built on it has no manifest an arm64 node can pull. 1.4.13 is an index
over both architectures.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 21:22:31 -07:00
zeekayandHanzo Dev 171fd1bd5d Move the workflows to .github/workflows
The forge reads .github/workflows natively and github.com reads nothing else,
so one directory serves both planes instead of two conventions that disagree
about which one is live. The reusable in hanzoai/ci already lives there, so a
caller naming that path now resolves to a file that exists.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 18:59:34 -07:00
zeekayandHanzo Dev f5ecb90561 ci: a runner label names the platform, not the org
hanzo-build-linux-amd64, lux-build-linux-amd64, zoo-build-linux-amd64 and
every other org-prefixed spelling all meant the same machine. Which org a
job belongs to does not change which host can build it, so the org bought
one pool name per org per arch and each could go stale on its own -- the
evo host alone answered to fourteen names for one label.

One vocabulary now: linux-amd64, linux-arm64, macos-arm64, win-amd64. The
generic labels every plain workflow uses (ubuntu-latest, self-hosted) are
untouched, so naive CI keeps working.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 12:30:47 -07:00
zeekayandHanzo Dev 0f6697b2e6 The registration door reads Create account, with the hint above it
The control keeps its two words and its button surface; "New here?"
stands as a line of its own. The refusal wears the note style, not the
footer's. @noble/curves stays on its 1.x line, which is the API the
connect package is written against.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 03:14:27 -07:00
zeekayandHanzo Dev a18ba03e53 Allow the three build scripts the install runs
pnpm 11 stops an install whose build scripts are left as placeholders;
sharp, tiny-secp256k1 and workerd are the ones this tree carries.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 03:02:49 -07:00
zeekayandHanzo Dev 309757d424 chore: symlink AGENTS.md + GEMINI.md + CLAUDE.md -> LLM.md
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 02:20:22 -07:00
zeekayandHanzo Dev 4043d05f4c the face is Zen, and it ships from @hanzo/font
One family. Font stacks name Zen and fall back to the native faces, tokens
name the role rather than a face, and the packages that used to supply a
face now point at @hanzo/font.

Self-hosted @font-face blocks are removed rather than renamed: rewriting the
family in one would leave a src pointing at a file this repo no longer ships.
@hanzo/font carries the binaries and declares the faces, so the local copies
go with the declaration that used them.

Comments naming a face we no longer use are deleted, not reworded.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 02:20:21 -07:00
zeekayandHanzo Dev 1b3ffa67dc the arm switch stops underlining
Nothing else on any Hanzo surface underlines — the console footer turns the
anchor default off and says why — and this rule was the one that did, so "Send me
a code instead" read as pasted in from another site next to the pill buttons and
the mark.

It is a quiet control and now says so the way the rest of the estate does: muted
until you point at it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 02:20:19 -07:00
zeekayandHanzo Dev 32adaffdc2 drop the retired-runner note
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 02:20:19 -07:00
zeekayandHanzo Dev 6c4672480f Name the reusable where the v1 tag keeps it
hanzoai/ci at v1 carries build.yml under .github/workflows; the .hanzo
copy lives only on a branch of the same name, so a run naming it is
refused before it starts.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 00:53:27 -07:00