configure-spectrum
v0.1.0-preview.6
Published
Configure identity and profile context for Photon Spectrum message agents.
Readme
Configure SSO for Spectrum message handlers
configure-spectrum adds Configure sign-in and profile access to an existing Photon Spectrum (spectrum-ts) message handler.
It resolves the current sender before your handler runs, then provides a Configure profile runtime for the right access state. Approved users can receive personalized responses on the first generated turn. New or unlinked senders get a stable developer-scoped profile and a hosted message sign-in path.
Spectrum continues to own channels, providers, webhooks, message objects, replies, typing, and delivery. This package only adds Configure identity and profile context at the message boundary.
Capabilities
- Identity context before the response. Resolve the sender before model execution, so your first generated reply can use the right identity state.
- Continuity across supported channels. When Spectrum exposes a phone-backed sender identifier, the adapter can resolve that sender to the same approved Configure user across supported channels. When a channel only exposes channel-local identifiers, the adapter falls back to a stable developer-scoped user until the sender links with Configure.
- One profile surface. Your handler uses
ctx.profilefor linked Configure users and unlinked developer-scoped users, so the agent code can stay consistent while access remains permission-aware. - Hosted message SSO. Generate or send
sign-in.melinks from the message thread. Configure handles verification, consent, connector setup, agent approval, and message return behavior when Spectrum exposes a reliable target. - No Spectrum replacement. Keep your existing
Spectrum()app, providers, webhook adapters, and message loop.
Install
Existing Spectrum apps can add the adapter:
npm install configure-spectrumNew apps should install Spectrum according to Photon's docs before adding this package. If your package manager does not auto-install peer dependencies, install spectrum-ts explicitly. This package depends on configure@^1.1.16 for the profile runtime plus hosted message URL, hosted completion, message-line registry, and sender-proof fallback helpers.
Existing Handler
import { withConfigure } from "configure-spectrum";
import { adapterStore } from "./configure-spectrum-store";
const configureSpectrum = withConfigure({
apiKey: process.env.CONFIGURE_API_KEY!,
publishableKey: process.env.CONFIGURE_PUBLISHABLE_KEY!,
agent: process.env.CONFIGURE_AGENT!,
store: adapterStore,
signIn: {
linkMode: "managed",
connectors: ["gmail", "calendar"],
},
connect: {
mode: "intent",
sendOnce: true,
behavior: "send-and-stop",
message: "Connect your Configure profile: {url}",
},
});
for await (const [space, message] of app.messages) {
await configureSpectrum.handle(space, message, async (ctx) => {
let configureReadUsed = false;
const tools = ctx.profile.tools({
connectors: ["gmail", "calendar"],
actions: ["email.send", "calendar.create_event"],
});
const reply = await runAgent({
message,
tools,
executeTool: async (toolCall) => {
const result = await ctx.profile.executeTool(toolCall);
if (isReadBackedConfigureTool(toolCall.name)) configureReadUsed = true;
return result;
},
linked: ctx.linked,
});
await message.reply(reply);
if (configureReadUsed) ctx.profile.commit({
messages: [
{ role: "user", content: ctx.text },
{ role: "assistant", content: reply },
],
}).catch(() => {});
});
}
function isReadBackedConfigureTool(name: string): boolean {
return name === "configure_profile_read" || name === "configure_profile_search";
}ctx.profile is built from a linked Configure token when one is available, or from a developer-scoped external user before sign-in. That lets the rest of your agent use one profile runtime while Configure enforces the appropriate access boundary.
Use ctx.profile.tools({ connectors, actions }) as the normal model-loop integration when your hosted Configure surface requested connector/action setup and your app supports those capabilities. Keep Configure tools available so the model can call configure_profile_read or configure_profile_search for overview, concrete memories, source-specific questions like "what does ChatGPT remember about me?", or details that need exact source attribution. Host-side ctx.profile.read({ sections }) plus profile.format() is available for app-owned UI, inspection, or explicit context slots; it is not required for the normal model loop. After a read-backed turn, call ctx.profile.commit() with bounded user/assistant turn evidence.
Action tools, such as sending email or creating calendar events, change external state. Tool visibility means hosted/app capability, not user authorization. Expose actions when the product has requested and supports that capability; ctx.profile.executeTool() still fails closed when linked state, connector state, permissions, scopes, or approval state are missing. If a connector/action is unavailable, send the hosted connect, reconnect, permissions, or approval link.
When connect sends a hosted link, handle() returns before the handler runs. The model does not need to decide when to produce Configure sign-in URLs.
Hosted links use Configure's public sign-in.me/{agent} surface, so the model never needs to construct Configure URLs. If your app passes signIn.agentPhone, that explicit app-bound line is used first. Otherwise, for Spectrum iMessage dedicated-line spaces, the adapter reads Spectrum's routed line from space.phone and uses it when it is a valid E.164 phone number. Shared-mode sentinels such as shared, blank local-mode values, and other non-phone values are ignored, so Configure falls back to the normal hosted completion path instead of receiving an unreliable return phone.
If Photon exposes the agent's current return line through an API instead of space.phone, pass an async resolver:
const configureSpectrum = withConfigure({
apiKey,
publishableKey,
agent: "your-agent",
store,
signIn: {
agentPhone: async (ctx) => photon.lines.currentPhone({ spaceId: ctx.space.id }),
},
});Set signIn.linkMode to "managed" to route message sign-in through Configure's message URL API. When a return line is available, the adapter registers that line for the configured agent before requesting the URL. Configure returns the hosted fallback when signed sender proof is missing or unsupported, and reserves code-bearing links for verified message senders. The older "auto" value is still accepted as a compatibility alias for "managed".
For production visibility, attach onEvent and send the redacted adapter events to your own telemetry sink:
const configureSpectrum = withConfigure({
apiKey,
publishableKey,
agent,
store,
signIn: { linkMode: "managed" },
onEvent(event) {
console.info("[configure]", {
event: event.event,
channel: event.channel,
identityState: event.identityState,
actionState: event.actionState,
outcome: event.outcome,
reason: event.reason,
properties: event.properties,
});
},
});The hook is application-owned. The adapter does not send telemetry to Configure. Events include states, counts, modes, booleans, and reason codes; they intentionally omit raw phone numbers, tokens, URLs, message bodies, connector payloads, and profile facts.
store persists adapter state between messages: sender mappings, approved Configure tokens, sign-in delivery state, completion journeys, and webhook idempotency. It does not store Configure user memories or profile data. Most apps back this with the same persistence they already use for sessions, users, or webhook idempotency.
For local development and examples:
const store = withConfigure.localStore();withConfigure.localStore() keeps adapter state in the current process. It resets when the worker restarts.
For the design rationale and message URL/reconnect implementation plan, see Message Auth Handoff Spec.
How Resolution Works
For each message, the adapter:
- Derives stable subject and thread keys from Spectrum
spaceandmessagemetadata. - Reuses a stored Configure token when the sender has already approved this agent.
- Checks Configure recognition when the channel exposes phone-backed sender identifiers.
- Falls back to a developer-scoped external user when the sender is not linked.
- Builds a
ctxobject for your handler before agent logic runs.
Recognition is not authorization. Treat ctx.linked as the signal that Configure has an approved agent token for this sender.
Handler Context
withConfigure().handle() gives your existing handler a ctx object with:
ctx.identity- the Configure identity result for this sender.ctx.profile- Configure profile runtime:read(),search(),remember(), tools, and tool execution.ctx.linked- true only when Configure has an approved agent token.ctx.recognized- true when Configure recognized sender evidence, even if the user has not approved this agent yet.ctx.signInUrl()- hosted message sign-in link for the current sender.ctx.replyWithSignIn()- convenience method for sending the hosted link in-thread.ctx.reconnectUrl({ connectors })- hosted reconnect link for refreshing a specific app connection.ctx.replyWithReconnect({ connectors })- convenience method for sending a reconnect link in-thread.ctx.subjectandctx.thread- stable keys for subject storage and thread-level app state.
Sign-In Handoff
The adapter can send hosted links before your model runs:
const configureSpectrum = withConfigure({
apiKey,
publishableKey,
agent,
store,
signIn: {
linkMode: "managed",
},
connect: {
mode: "intent",
intent: /\b(connect|link|sign[\s-]?in|login)\b/i,
sendOnce: true,
behavior: "send-and-stop",
message: "Connect your Configure profile: {url}",
},
});connect.mode defaults to manual, so the adapter does not send links unless your app opts in. Use mode: "first-message" if your product should require Configure sign-in before the first model response.
Message Return Behavior
Developers should not need to know Configure hosted URL parameters to return a user to the same message channel after sign-in. The adapter infers that from the current Spectrum space and message when Spectrum exposes a reliable target.
For iMessage dedicated-line spaces, Spectrum includes the routed sending line on space.phone. When no explicit signIn.agentPhone is configured and that value is a valid E.164 phone number, the adapter passes it to Configure for sign-in and reconnect links. In shared iMessage mode, local mode, or channels without a reliable message return target, the adapter omits the return phone and keeps the hosted Configure completion fallback.
signIn.agentPhone is the explicit app-bound return line. It can be a string or an async resolver that calls Photon for the current line. The resolved value is validated as E.164; values such as shared are ignored rather than sent to Configure.
In linkMode: "managed", valid return lines are registered through Configure before the adapter asks for a message URL. If registration or message URL creation fails, the adapter omits return-phone metadata from its local fallback and keeps the hosted sign-in path usable.
You can still send a link manually from application code:
if (!ctx.linked && needsPersonalData(ctx)) {
await ctx.replyWithSignIn();
return;
}When a Configure-backed tool reports that provider access needs to be refreshed, send a targeted reconnect link from application code:
await ctx.replyWithReconnect({
connectors: ["gmail"],
message: "Reconnect Gmail so I can keep helping with email: {url}",
});
return;Reconnect links use the same hosted surface as sign-in, but they only refresh the requested app connection. In linkMode: "managed", the adapter asks Configure for a message URL and accepts the plain hosted reconnect URL unless verified Spectrum sender proof allows a code-bearing link.
Webhook Composition
Compose this adapter inside Spectrum's webhook adapters. Spectrum should handle raw body parsing, signature verification, and provider normalization.
import express from "express";
import { Spectrum } from "spectrum-ts";
import { spectrum } from "@spectrum-ts/express";
import { withConfigure } from "configure-spectrum";
const app = await Spectrum({
webhookSecret: process.env.SPECTRUM_WEBHOOK_SECRET!,
providers: [],
});
const configureSpectrum = withConfigure({
apiKey: process.env.CONFIGURE_API_KEY!,
publishableKey: process.env.CONFIGURE_PUBLISHABLE_KEY!,
agent: process.env.CONFIGURE_AGENT!,
store,
});
const server = express();
server.use(
spectrum({
app,
onMessage: async (space, message) => {
await configureSpectrum.handle(space, message, runAgent);
},
})
);Production Checklist
- Use Spectrum's webhook adapters for webhook verification and raw body handling.
- Provide a
storeimplementation backed by your app's normal persistence layer. - Implement
claimMessage()for webhook idempotency. - Implement
saveJourney()andconsumeJourney()before settingmessageCompleteUrl. - Choose a stored-token validation policy and document it.
- Keep
CONFIGURE_API_KEYserver-side. - Keep
CONFIGURE_PUBLISHABLE_KEYbrowser-safe; the adapter's default plain message link does not need to expose it in the URL. - Pass
signIn.agentPhonewhen your app has an authoritative line binding; otherwise let the adapter infer iMessage return lines from Spectrumspace.phonewhen possible. Do not pass shared-mode sentinels as phone numbers. - Do not log tokens, phone numbers, full message bodies, or webhook headers.
- Do not treat phone recognition as linked access unless Configure returns an approved token.
Boundaries
- This package does not construct a Spectrum app.
- This package does not re-export Spectrum providers, content builders, webhook adapters, or runtime APIs.
- This package does not parse or verify Photon webhooks.
- This package does not own your model loop, queue, retry worker, or outbound outbox.
spectrum-tsis a peer dependency. Existing apps keep their own compatible Spectrum version.
Stability
This package is pre-1.0. APIs may change before 1.0; breaking changes will be documented in CHANGELOG.md.
