Validating received state
Handoff hands you JSON. Your schema decides if it is your JSON.
Handoff owns moving the bytes. Your application owns what they mean —
which is why receive() returns HandoffState (JSON), not your types.
Version your own payload inside the state, separately from the protocol:
// sending
await handoff.create({
type: "draft",
version: 2,
data: { text, updatedAt },
});The ho1_ protocol version belongs to Handoff; type and version
belong to you. A v1 artifact can carry your version-7 payload — the two
evolve independently.
Parse, don't trust
Received state is untrusted input arriving via a URL anyone can craft:
import { z } from "zod";
const draftSchema = z.object({
type: z.literal("draft"),
version: z.literal(2),
data: z.object({ text: z.string().max(10_000), updatedAt: z.number() }),
});
const raw = await handoff.receive();
const draft = draftSchema.parse(raw); // throws on anything unexpectedAny schema library works — zod, valibot, arktype, or a hand-written guard. Handoff deliberately depends on none of them and has no registry of application schemas; a transfer library that understood your domain would be a framework.
Handle the failure paths a user will actually hit
import { isHandoffError } from "@0xsarwagya/handoff";
try {
const draft = draftSchema.parse(await handoff.receive());
} catch (error) {
if (isHandoffError(error)) {
// truncated scan, stale link, artifact from a newer app version …
show(`This handoff could not be read (${error.code}).`);
} else {
// valid artifact, unexpected shape — likely a version skew
show("This handoff came from an incompatible version of the app.");
}
}A half-scanned QR code and a link mangled by a chat app are normal events,
not edge cases. Both surface as typed HandoffErrors — give them a human
sentence.