Shared building blocks for Spectrum client-side
adapters — the small packages that consume Fusor
webhook deliveries and integrate a messaging platform with the spectrum-ts SDK
via defineFusorPlatform. Extracted from the reference adapters
(@photon-ai/linq,
@photon-ai/telegram) so each one isn't
re-implementing the same webhook plumbing.
spectrum-ts is a peer dependency; there are no runtime dependencies.
bun add @photon-ai/adapter-core spectrum-tsimport { safeEqualHex, safeEqualUtf8, hmacHex, verifyHmac, assertFresh } from "@photon-ai/adapter-core";
// HMAC schemes (e.g. LinQ signs `${timestamp}.${rawBody}`):
assertFresh(timestamp, 300); // replay guard — throws if stale / non-numeric
if (!verifyHmac({ secret, signedValue: `${timestamp}.${bodyText}`, signature })) {
throw new Error("…webhook signature mismatch");
}
// Shared secret-token schemes (e.g. Telegram's X-Telegram-Bot-Api-Secret-Token):
if (!safeEqualUtf8(headerToken, config.secretToken)) {
throw new Error("…secret token mismatch");
}All comparisons are constant-time and return false (never throw) on unequal /
zero length — they leak only length equality, never contents.
createClient returns the inbound Fusor brand, so build your outbound client
once and cache it on the per-platform store:
import { createClientCache, type StoreLike } from "@photon-ai/adapter-core";
const clientCache = createClientCache<MyClient>("myplatform.client");
export const initClient = (store: StoreLike, config: MyConfig) =>
clientCache.init(store, () => makeClient(config)); // in lifecycle.createClient
export const getClient = (store: StoreLike, config: MyConfig) =>
clientCache.get(store, () => makeClient(config)); // in sendStoreLike is the structural shape of spectrum-ts's internal store (which it
does not export as a type).
import { boundedSeenSet } from "@photon-ai/adapter-core";
const seenSet = boundedSeenSet(); // bounded, insertion-order eviction
if (seenSet.seen(message.id)) return; // Fusor delivery is at-least-onceimport { passthrough } from "@photon-ai/adapter-core";
await message.reply(passthrough(message.content)); // echo an inbound Content backimport { makeFusorLifecycle } from "@photon-ai/adapter-core";
defineFusorPlatform(MY_PLATFORM, {
// …
lifecycle: makeFusorLifecycle({ name: MY_PLATFORM, verify: makeVerify, init: initClient }),
});Equivalent to wiring fusor(name, makeVerify(config)) by hand inside
createClient; use it only to trim the repeated boilerplate.
spectrum-ts— the SDK and thedefineFusorPlatform/fusorcontract.fusorclient-adapters guide — how an adapter consumes Fusor's delivery.