An audience proves nothing alone: any client IAM authenticates may mint a token
for this app's resource. So the bearer is an application's access token whose
azp IAM_APP_TEAMS lists, and it acts for exactly the team that list maps it to,
as that team's organisation owner. People sign in; the person bearer and the
X-Team-Id choice are gone, and so is IAM_ORG. A sign-in's id_token is never a
bearer.
A webhook carries eSign's identity only to the Hanzo API, minted for it; any
other endpoint gets the webhook's own secret and nothing of ours.
A call without a session carries a Hanzo IAM access token: signed by a key in
hanzo.id's JWKS, issued by hanzo.id, minted for this app (aud = IAM_CLIENT_ID)
and unexpired. One resolver, getBearer, sits behind /v1/rpc, /v1/rest and the
download routes, and acts in the team X-Team-Id names: a person as the account
their sign-in linked, an application of IAM_ORG as the team's organisation
owner. API tokens no longer get into the API; Zapier, which could carry nothing
else, is deleted.
Outbound, one Machine mints eSign's client_credentials token for the service it
is spent at. notify uses it as before, and every webhook now carries one minted
for its endpoint's origin, so a Hanzo receiver verifies the caller against
hanzo.id instead of holding a shared secret.
Emails, the signing page's title, the sealed PDF's reason, the sign-in
refusal and the dialogs still said Hanzo Sign. The catalogs change with
the strings, message for message, so every translation keeps its id.
The resource API is /v1/rest and the operation API is /v1/rpc: named
for what they are, so neither shadows the other, and the v2-beta alias
is gone. Hanzo IAM returns to /auth/callback, the callback path IAM
already derives for this host, so sign-in needs no IAM change.
Claude-Session: https://claude.ai/code/session_01TUxvfuta1Z44KVNYuAz71F
The suppression count for this work is `git grep -c` over '@ts-ignore',
'@ts-expect-error', 'as any', 'as unknown as', ': any' and eslint-disable.
Three comments added in it matched as prose: a quoted index signature in
the v1 contract, "over: any string" in the upload test and "has any meta"
in the field tooltip. They say the same thing in words that do not match,
so the counts read 10 `as any` and 8 `: any`, as before this work.
Co-authored-by: Hanzo Dev <[email protected]>
ESLint 10's recommended set added no-useless-assignment, which fails on a
value no later statement can read. Fourteen places had one:
- An initial value every path overwrites before reading: `let x = null`
ahead of a try that assigns it or leaves, the webhook response body both
branches set, the rate-limit loop's counter, the avatar's previous id and
the license response. They are declared without one; TypeScript still
checks each is assigned before use.
- Konva stages and layers set to null after destroy(), just before the
function returns and the variable goes out of scope. The nulls go, and
the variables are const.
- Two in the v1 PDF field writer: textFieldBoxY recomputed after the only
read of it had already been copied into adjustedFieldY, and a line reset
to '' that both following branches overwrite.
- The folder breadcrumbs' currentFolderId, reassigned in a loop that walks
by currentFolder.parentId; the first query reads folderId directly.
No output changes: each removed value was provably never read.
Co-authored-by: Hanzo Dev <[email protected]>
The v1 contract went through ts-rest's c.router with the authorization
header as baseHeaders. @ts-rest/core 3.52's router types merge that header
through zod 3 types — AnyZodObject, objectUtil.MergeShapes, a ZodObject of
three type arguments — which zod 4 does not have. Behind skipLibCheck each
one is `any`, and the route types they build collapse to
`{ [x: string]: any; method; query }`: no path, no body, no responses. Every
handler lost its params, query and body — 56 errors in the implementation —
and every example's client call stopped being callable.
The routes are now a plain object `as const satisfies AppRouter`, each
naming the header itself. At run time c.router did nothing else here: it
merged baseHeaders into routes that had none, which is the header schema
itself, and copied responses. ts-rest's server and client types read the
plain object correctly under zod 4, and all 19 routes type their handlers.
Two examples were wrong once their types could be read: sendDocument
requires a body — `{}` takes the defaults the server would apply — and
createField answers `{ fields, documentId }`, not a field, so the example
reads the field out of `fields`.
Co-authored-by: Hanzo Dev <[email protected]>
The v1 API's schemas called extendZodWithOpenApi(z) from
@anatine/zod-openapi at load and then .openapi({...}) on six fields.
anatine 1.14 is written for zod 3: it patches z.ZodSchema.prototype, and in
zod 4 ZodSchema is a type with no value behind it. Under zod 4 the call
throws "Cannot read properties of undefined (reading 'prototype')" when
schema.ts loads — and schema.ts loads with the contract, the
implementation and the OpenAPI document, which the app's server router
imports, so since the version sweep that router could not load. The types
said as much: `openapi` does not exist on a zod 4 schema.
The extension goes. The four descriptions and the two lists of allowed time
zones and date formats move to .meta(), which zod 4 keeps in its registry;
the two empty .openapi({}) calls carried nothing and go. No parse changes.
Not fixed here: @ts-rest/open-api 3.52 still converts schemas through
anatine, which reads zod 3's internals, so /api/v1/openapi.json now loads
but every schema in it is empty. Before the sweep it had them.
Co-authored-by: Hanzo Dev <[email protected]>
Envelope id allocation now establishes the counter row it reads, so the
first document on a database that has never allocated one gets id 1.
Nothing inside a transaction reaches the global Prisma client any more.
Base SQLite serves one write connection, so a second client waiting on
the lock the transaction already holds stalls until the busy timeout and
the transaction dies. Webhooks, job triggers and verification emails are
side effects of committed state, so they fire after the commit; the
organisation link no longer nests a second transaction inside itself; and
a recipient lookup in the v1 field API reads on its own transaction.
EXISTS subqueries name a column. Postgres accepts an empty select list,
SQLite does not, which is what stopped the seal sweep, the unsealed
document view and signing volume from running at all.
Also:
- the profile form saves a name before a signature exists, matching the
server schema, instead of failing validation with nothing to show
- every upload reads its error from one place, so a failed template
upload says template
- the documents filter strip scrolls its selected tab into view
- the inbox and support headings use the shared page header
- one settings nav item, so a click lands on the link and fires once
- the command dialog names itself for screen readers, and the mobile
search and menu buttons carry labels
- documentation links point at docs.hanzo.ai
- the organisations row links to the organisation
- the app is called Hanzo Sign throughout, the wordmark variables are
declared to turbo, and the webhook secret rides a valid header name
The product is branded Hanzo eSign (esign.hanzo.ai, repo hanzoai/esign).
Rename all workspace packages @hanzo/sign-* → @hanzo/esign-* (1052 files)
and align docker/Dockerfile turbo --scope/--filter to the actual remix
package name @hanzo/esign (was a pre-existing mismatch with -remix that
broke the build). Protected deps (@hanzo/insights, @hanzo/logo) untouched.
Delete packages/ee/ and packages/trpc/server/enterprise-router/. The features
those directories gated — limits, custom email domains, SAML/SSO authentication
portal, account linking — are now permanently available to every user. Stripe
billing surface (subscriptions, plans, invoices, paywall webhook) is gone in
its entirety.
- Move limits provider/server/client/handler/schema/constants from
packages/ee/server-only/limits/ into packages/lib/server-only/limits/.
getServerLimits always returns the self-hosted unlimited plan; no plan tier
lookup, no IS_BILLING_ENABLED gating.
- Move email-domain helpers (create/delete/verify/reregister) from
packages/ee/server-only/lib/ into packages/lib/server-only/email-domain/.
- Move link-organisation-account + send-organisation-account-link-confirmation
-email from packages/ee/server-only/lib/ into
packages/lib/server-only/organisation/.
- Move every non-stripe route from packages/trpc/server/enterprise-router/
into packages/trpc/server/organisation-router/ and expose them at
trpc.organisation.{email,emailDomain,authenticationPortal}.*.
- Delete the entire stripe subtree (ee + lib stripe SDK + lib billing utils
+ apps/remix stripe.webhook route).
- IS_BILLING_ENABLED() now always returns false; downstream paywall branches
become dead code that always opens the feature up.
- Strip the billing settings page and dialog, billing banner/portal button,
billing-plans selector, and stripe-customer admin route.
API users were inadvertently sending documents without signature fields,
causing confusion for recipients and breaking their signing flows.
- Add getRecipientsWithMissingFields helper in recipients.ts
- Add server-side validation in sendDocument to block distribution
- Fix v1 API to return 400 instead of 500 for validation errors
- Consolidate UI signature field checks to use isSignatureFieldType
- Add E2E tests for both v1 and v2 APIs
This PR is handles the changes required to support envelopes. The new
envelope editor/signing page will be hidden during release.
The core changes here is to migrate the documents and templates model to
a centralized envelopes model.
Even though Documents and Templates are removed, from the user
perspective they will still exist as we remap envelopes to documents and
templates.
Adds password reauthentication to our existing reauth providers,
additionally swaps from an exclusive provider to an inclusive type where
multiple methods can be selected to offer a this or that experience.
Adds next recipient dictation functionality to document signing flow,
allowing assistants and signers to update the next recipient's
information during the signing process.
## Related Issue
N/A
## Changes Made
- Added form handling for next recipient dictation in signing dialogs
- Implemented UI for updating next recipient information
- Added e2e tests covering dictation scenarios:
- Regular signing with dictation enabled
- Assistant role with dictation
- Parallel signing flow
- Disabled dictation state
## Testing Performed
- Added comprehensive e2e tests covering:
- Sequential signing with dictation
- Assistant role dictation
- Parallel signing without dictation
- Form validation and state management
- Tested on Chrome and Firefox
- Verified recipient state updates in database
Improves the existing document rejection process by actually marking a
document as completed cancelling further actions.
## Related Issue
N/A
## Changes Made
- Added a new rejection status for documents
- Updated a million areas that check for document completion
- Updated email sending, so rejection is confirmed for the rejecting
recipient while other recipients are notified that the document is now
cancelled.
## Testing Performed
- Ran the testing suite to ensure there are no regressions.
- Performed manual testing of current core flows.