Files
zooqueenandHanzo Dev ecee5e40aa Sign in through Hanzo IAM only, and make accounts for invited people only
The IAM callback at /auth/callback reads IAM's userinfo answer and refuses
an address IAM has not verified. An account is found by its linked IAM
identity alone (provider hanzo, the IAM subject); an existing account that
only shares the address is refused, never merged. A first sign-in makes an
account only for an admin of the org the application serves or for someone
an organisation invited at that verified address. Every refusal returns to
/signin with a sentence saying what to do.

Accepting an organisation invitation joins the signed-in person, and only
when their verified address is the invited one; opening the link no longer
adds whoever holds the address.

The local email/password sign-up, sign-in, reset, verify-email and update
routes, pages, jobs and mail are gone, as are passkey sign-in, the Google,
Microsoft and generic OIDC providers, the per-organisation SSO portal and
its account-link flow. Passkeys and two-factor stay for confirming a
signature. The e2e suite starts sessions the way a finished IAM sign-in
leaves them.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 01:52:24 -07:00

105 lines
3.8 KiB
TypeScript

// esign ZAP handlers — auth router: the signed-in person's passkeys.
//
// Verbatim tRPC procedure bodies rewired to (ctx, raw). Input is validated by
// the SAME Zod schema the tRPC procedure used; the server-only functions are
// reused unchanged. Routes:
// auth.passkey.create / createAuthenticationOptions /
// createRegistrationOptions / delete / find / update.
//
// A passkey re-authenticates a signed-in person for an action; it is never a
// way to sign in, which is Hanzo IAM's alone.
import type { RegistrationResponseJSON } from '@simplewebauthn/server';
import { createPasskey } from '@hanzo/esign-lib/server-only/auth/create-passkey';
import { createPasskeyAuthenticationOptions } from '@hanzo/esign-lib/server-only/auth/create-passkey-authentication-options';
import { createPasskeyRegistrationOptions } from '@hanzo/esign-lib/server-only/auth/create-passkey-registration-options';
import { deletePasskey } from '@hanzo/esign-lib/server-only/auth/delete-passkey';
import { findPasskeys } from '@hanzo/esign-lib/server-only/auth/find-passkeys';
import { updatePasskey } from '@hanzo/esign-lib/server-only/auth/update-passkey';
import { ZCreatePasskeyAuthenticationOptionsRequestSchema } from '../../../server/auth-router/create-passkey-authentication-options.types';
import { ZCreatePasskeyRegistrationOptionsRequestSchema } from '../../../server/auth-router/create-passkey-registration-options.types';
import { ZCreatePasskeyRequestSchema } from '../../../server/auth-router/create-passkey.types';
import { ZDeletePasskeyRequestSchema } from '../../../server/auth-router/delete-passkey.types';
import { ZFindPasskeysRequestSchema } from '../../../server/auth-router/find-passkeys.types';
import { ZUpdatePasskeyRequestSchema } from '../../../server/auth-router/update-passkey.types';
import type { ZapContext } from '../context';
import type { ZapRouteMap } from '../dispatch';
export const authRoutes: ZapRouteMap = {
'auth.passkey.create': async (ctx: ZapContext, raw) => {
const input = ZCreatePasskeyRequestSchema.parse(raw);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const verificationResponse = input.verificationResponse as RegistrationResponseJSON;
return await createPasskey({
userId: ctx.user.id,
verificationResponse,
passkeyName: input.passkeyName,
requestMetadata: ctx.metadata.requestMetadata,
});
},
'auth.passkey.createAuthenticationOptions': async (ctx: ZapContext, raw) => {
const input = ZCreatePasskeyAuthenticationOptionsRequestSchema.parse(raw);
return await createPasskeyAuthenticationOptions({
userId: ctx.user.id,
preferredPasskeyId: input?.preferredPasskeyId,
});
},
'auth.passkey.createRegistrationOptions': async (ctx: ZapContext, raw) => {
ZCreatePasskeyRegistrationOptionsRequestSchema.parse(raw);
return await createPasskeyRegistrationOptions({
userId: ctx.user.id,
});
},
'auth.passkey.delete': async (ctx: ZapContext, raw) => {
const { passkeyId } = ZDeletePasskeyRequestSchema.parse(raw);
ctx.logger.info({
input: {
passkeyId,
},
});
await deletePasskey({
userId: ctx.user.id,
passkeyId,
requestMetadata: ctx.metadata.requestMetadata,
});
},
'auth.passkey.find': async (ctx: ZapContext, raw) => {
const { page, perPage, orderBy } = ZFindPasskeysRequestSchema.parse(raw);
return await findPasskeys({
page,
perPage,
orderBy,
userId: ctx.user.id,
});
},
'auth.passkey.update': async (ctx: ZapContext, raw) => {
const { passkeyId, name } = ZUpdatePasskeyRequestSchema.parse(raw);
ctx.logger.info({
input: {
passkeyId,
},
});
await updatePasskey({
userId: ctx.user.id,
passkeyId,
name,
requestMetadata: ctx.metadata.requestMetadata,
});
},
};