mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
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.
98 lines
4.0 KiB
TypeScript
98 lines
4.0 KiB
TypeScript
// esign ZAP RPC — JSON-over-HTTP face for the /v1/rpc operation API.
|
|
//
|
|
// The operation API (external integrators) was backed by
|
|
// trpc-to-openapi routing HTTP -> the tRPC appRouter. This re-backs it with
|
|
// `httpServe` from @zap-proto/web/server, dispatching every request through the
|
|
// SAME zapRoutes map + makeDispatcher the WebSocket server uses — one service
|
|
// implementation, reached two ways (WS and HTTP). NO tRPC.
|
|
//
|
|
// Each OpenAPI operation becomes one POST HttpRoute whose codec mirrors the
|
|
// browser ZAP client (zap/client): the JSON body is wrapped into a ZapRequest
|
|
// envelope carrying the route string (the operationId) as `method`, the payload
|
|
// as a SuperJSON string, under the single METHOD_RPC ordinal; the ZapReply is
|
|
// unwrapped back to JSON (or the reconstructed AppError is thrown).
|
|
//
|
|
// Auth is per-request via makeMintCap('apiV2'), which reads
|
|
// "Authorization: Bearer <token>" off the IncomingMessage and resolves it via
|
|
// getBearer — identical to the tRPC apiV2 context. httpServe hands the
|
|
// mint the raw upgrade/request auth boundary, so no second auth path exists.
|
|
import type { Server as HttpServer } from 'node:http';
|
|
|
|
import { type HttpRoute, type HttpServeHandle, httpServe } from '@zap-proto/web/server';
|
|
import SuperJSON from 'superjson';
|
|
|
|
import { makeDispatcher } from '@hanzo/esign-trpc/zap/server/dispatch';
|
|
import { fromWireError } from '@hanzo/esign-trpc/zap/runtime/error';
|
|
import { METHOD_RPC } from '@hanzo/esign-trpc/zap/runtime/method';
|
|
import { makeMintCap } from '@hanzo/esign-trpc/zap/server/mint';
|
|
import { publicRoutes, zapRoutes } from '@hanzo/esign-trpc/zap/server/routes';
|
|
import { ZapReply, newZapRequest } from '@hanzo/esign-trpc/zap/gen/transport_zap';
|
|
|
|
import openapi from '@hanzo/esign-trpc/zap/gen/openapi.json';
|
|
|
|
// Re-export the WebSocket ZAP server (serveZap) through this module so the
|
|
// raw-copied server/main.js imports BOTH serveZap and serveZapHttpApi from the
|
|
// single rollup-bundled ./hono/server/zap/http-api.js — keeping the first-party
|
|
// esign ZAP server layer fully bundled (it can't resolve as a .ts workspace
|
|
// subpath at runtime). @zap-proto/web stays an external registry dep.
|
|
export { serveZap } from '@hanzo/esign-trpc/zap/server';
|
|
|
|
interface OpenApiPaths {
|
|
paths: Record<string, { post?: { operationId?: string } }>;
|
|
}
|
|
|
|
/**
|
|
* Build one HttpRoute per `(path, post.operationId)` in the composite OpenAPI
|
|
* doc. The route string carried on the envelope is the operationId
|
|
* ("<router>.<procedure>"), which is exactly the key zapRoutes is registered
|
|
* under — so the HTTP face and the WS face resolve to the identical handler.
|
|
*/
|
|
function buildRoutes(): HttpRoute[] {
|
|
const { paths } = openapi as OpenApiPaths;
|
|
const routes: HttpRoute[] = [];
|
|
|
|
for (const [path, item] of Object.entries(paths)) {
|
|
const operationId = item.post?.operationId;
|
|
if (!operationId) continue;
|
|
|
|
routes.push({
|
|
path,
|
|
method: METHOD_RPC,
|
|
decode: (json: unknown): Uint8Array =>
|
|
newZapRequest({
|
|
method: operationId,
|
|
payload: json === undefined || json === null ? '' : SuperJSON.stringify(json),
|
|
teamId: '',
|
|
}),
|
|
encode: (body: Uint8Array): unknown => {
|
|
const reply = ZapReply.wrap(body);
|
|
if (!reply.ok) {
|
|
throw fromWireError(reply.status, reply.errorJson);
|
|
}
|
|
return reply.result === '' ? undefined : SuperJSON.parse(reply.result);
|
|
},
|
|
});
|
|
}
|
|
|
|
return routes;
|
|
}
|
|
|
|
/**
|
|
* Mount the JSON-over-HTTP ZAP face on `httpServer` at `/v1/rpc`, sharing the
|
|
* WS server's port. httpServe only terminates its declared POST routes and
|
|
* passes every other request through, so the Hono GET `/v1/rpc/openapi.json`
|
|
* and the GET download routes still reach Hono.
|
|
*/
|
|
export function serveZapHttpApi(
|
|
httpServer: HttpServer,
|
|
opts: { onError?: (err: unknown) => void } = {},
|
|
): HttpServeHandle {
|
|
return httpServe(httpServer, {
|
|
prefix: '/v1/rpc',
|
|
routes: buildRoutes(),
|
|
mintCap: makeMintCap('apiV2'),
|
|
rootCap: makeDispatcher(zapRoutes, publicRoutes),
|
|
onError: opts.onError,
|
|
});
|
|
}
|