Skip to content

Commit 0c44731

Browse files
committed
fix: resolve session userId, guard provision, fallback to shared bot number
1 parent ecfc201 commit 0c44731

4 files changed

Lines changed: 109 additions & 29 deletions

File tree

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,9 @@ MASTER_KEY=
1717
# Default model used to answer iMessage threads.
1818
CODEX_MODEL=gpt-5-codex
1919

20+
# Optional: Spectrum shared-pool inbound number to display when the project is on
21+
# the free plan and Spectrum doesn't return a per-user bot number. E.164 format.
22+
# SPECTRUM_SHARED_IMESSAGE_NUMBER=+14155550123
23+
2024
# Container entrypoint mode: "webapp" or "bridge".
2125
PROCESS=webapp

app/api/provision/route.ts

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getProject,
1010
getSession,
1111
imessageRedirectUrl,
12+
listSpectrumUsers,
1213
provisionImessage,
1314
regenerateProjectSecret,
1415
togglePlatform,
@@ -77,6 +78,14 @@ export async function POST(req: Request) {
7778
openaiKey?: unknown;
7879
userPhone?: unknown;
7980
};
81+
console.log(
82+
"[provision] body received: openaiKey=",
83+
typeof body.openaiKey === "string"
84+
? `present(len=${body.openaiKey.length})`
85+
: typeof body.openaiKey,
86+
"userPhone=",
87+
typeof body.userPhone === "string" ? `"${body.userPhone}"` : typeof body.userPhone,
88+
);
8089
if (typeof body.userPhone === "string" && body.userPhone.trim().length > 0) {
8190
userPhone = body.userPhone.trim();
8291
}
@@ -113,6 +122,17 @@ export async function POST(req: Request) {
113122
const ownerPhone =
114123
userPhone ?? (typeof session.user.phoneNumber === "string" ? session.user.phoneNumber : null);
115124

125+
if (!ownerPhone) {
126+
return NextResponse.json(
127+
{
128+
error:
129+
"We need your phone number to assign you a Spectrum iMessage bot. Add it and continue.",
130+
reason: "phone_required",
131+
},
132+
{ status: 422 },
133+
);
134+
}
135+
116136
const db = getDb();
117137

118138
const existing = await db
@@ -150,17 +170,6 @@ export async function POST(req: Request) {
150170
`[provision] dashboard id=${project.id} cloud spectrumProjectId=${details.spectrumProjectId ?? "(missing — using dashboard id)"}`,
151171
);
152172

153-
if (!ownerPhone) {
154-
return NextResponse.json(
155-
{
156-
error:
157-
"We need your phone number to assign you a Spectrum iMessage bot. Add it and continue.",
158-
reason: "phone_required",
159-
},
160-
{ status: 422 },
161-
);
162-
}
163-
164173
const fullName = session.user.name?.trim() ?? "";
165174
const [firstName, ...rest] = fullName.split(/\s+/);
166175
const userResp = await createSpectrumUser(bearer, project.id, {
@@ -172,29 +181,46 @@ export async function POST(req: Request) {
172181
});
173182
console.log("[provision] create-spectrum-user response:", JSON.stringify(userResp));
174183

175-
let line = await provisionImessage(bearer, cloudProjectId, projectSecret).catch(
176-
(err: unknown) => {
184+
let line: { id: string; phoneNumber: string } | null = null;
185+
186+
const fromUser = pickPhoneFrom(userResp, ownerPhone);
187+
if (fromUser) {
188+
line = { id: userResp.user?.id ?? `${cloudProjectId}:imessage`, phoneNumber: fromUser };
189+
console.log(`[provision] picked bot number from user-add response: ${fromUser}`);
190+
}
191+
192+
if (!line) {
193+
try {
194+
line = await provisionImessage(bearer, cloudProjectId, projectSecret);
195+
console.log(`[provision] picked bot number from provisionImessage: ${line.phoneNumber}`);
196+
} catch (err) {
177197
console.warn(
178-
"[provision] provisionImessage fallback after user-add:",
198+
"[provision] provisionImessage didn't yield a number:",
179199
err instanceof Error ? err.message : err,
180200
);
181-
return null;
182-
},
183-
);
201+
}
202+
}
184203

185204
if (!line) {
186-
const fromUser = pickPhoneFrom(userResp, ownerPhone);
187-
if (fromUser) {
188-
line = { id: userResp.user?.id ?? `${cloudProjectId}:imessage`, phoneNumber: fromUser };
189-
} else {
190-
throw new SpectrumError(
191-
"Couldn't read the assigned iMessage number from Spectrum's response.",
192-
500,
193-
userResp,
194-
);
205+
const usersList = await listSpectrumUsers(bearer, project.id);
206+
if (usersList) {
207+
console.log("[provision] users list response:", JSON.stringify(usersList).slice(0, 1500));
208+
const fromList = pickPhoneFrom(usersList, ownerPhone);
209+
if (fromList) {
210+
line = { id: `${cloudProjectId}:imessage`, phoneNumber: fromList };
211+
console.log(`[provision] picked bot number from users list: ${fromList}`);
212+
}
195213
}
196214
}
197215

216+
if (!line) {
217+
throw new SpectrumError(
218+
"Couldn't read the assigned iMessage number from Spectrum. Set SPECTRUM_SHARED_IMESSAGE_NUMBER to the shared inbound number, or check the Spectrum dashboard.",
219+
500,
220+
{ userResp },
221+
);
222+
}
223+
198224
const projectSecretBlob = encrypt(projectSecret);
199225
const openaiBlob = openaiKey ? encrypt(openaiKey) : null;
200226

app/onboard/onboard-client.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ export default function OnboardClient() {
141141

142142
useEffect(() => {
143143
if (stage !== "provision") return;
144+
if (!userPhone.trim()) {
145+
setStage("phone");
146+
return;
147+
}
144148
let cancelled = false;
145149
setBusy(true);
146150
void fetch("/api/provision", {

lib/spectrum.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,38 @@ export interface SessionUser {
138138
[key: string]: unknown;
139139
}
140140

141+
interface RawSessionEnvelope {
142+
user?: Partial<SessionUser> & Record<string, unknown>;
143+
session?: { userId?: string; id?: string; [key: string]: unknown };
144+
[key: string]: unknown;
145+
}
146+
141147
export async function getSession(bearer: string): Promise<{ user: SessionUser } | null> {
142148
const res = await fetch(`${dashboardHost()}/api/auth/get-session`, {
143149
headers: { authorization: `Bearer ${bearer}` },
144150
});
145151
if (res.status === 401) return null;
146-
const body = (await expectOk<{ user?: SessionUser }>(res, "get-session")) ?? {};
147-
if (!body.user) return null;
148-
return { user: body.user };
152+
const body = (await expectOk<RawSessionEnvelope>(res, "get-session")) ?? {};
153+
const rawUser = body.user;
154+
if (!rawUser) return null;
155+
156+
const id =
157+
(typeof rawUser.id === "string" && rawUser.id) ||
158+
(typeof body.session?.userId === "string" && body.session.userId) ||
159+
(typeof body.session?.id === "string" && body.session.id) ||
160+
null;
161+
162+
if (!id) {
163+
console.warn("[spectrum] get-session returned user without id; body:", JSON.stringify(body));
164+
return null;
165+
}
166+
167+
return {
168+
user: {
169+
...(rawUser as Record<string, unknown>),
170+
id,
171+
} as SessionUser,
172+
};
149173
}
150174

151175
export interface SpectrumUserResult {
@@ -195,6 +219,23 @@ export async function createSpectrumUser(
195219
return body ?? {};
196220
}
197221

222+
export async function listSpectrumUsers(
223+
bearer: string,
224+
projectId: string,
225+
): Promise<Record<string, unknown> | unknown[] | null> {
226+
try {
227+
const res = await fetch(
228+
`${dashboardHost()}/api/projects/${encodeURIComponent(projectId)}/spectrum/users`,
229+
{ headers: { authorization: `Bearer ${bearer}` } },
230+
);
231+
if (!res.ok) return null;
232+
return (await asJson(res)) as Record<string, unknown> | unknown[] | null;
233+
} catch (err) {
234+
console.warn("[spectrum] list-users probe failed:", err instanceof Error ? err.message : err);
235+
return null;
236+
}
237+
}
238+
198239
export interface CreateProjectInput {
199240
name: string;
200241
location?: string;
@@ -551,6 +592,11 @@ export async function provisionImessage(
551592
const dashboardScan = await findAssignedImessageNumber(bearer, projectId);
552593
if (dashboardScan) return dashboardScan;
553594

595+
const sharedFallback = process.env.SPECTRUM_SHARED_IMESSAGE_NUMBER?.trim();
596+
if (tokens.type === "shared" && sharedFallback) {
597+
return { id: `${projectId}:imessage:shared`, phoneNumber: sharedFallback };
598+
}
599+
554600
throw new SpectrumError(
555601
`iMessage activated but no phone number was returned (type=${tokens.type}). Check the Spectrum dashboard for the assigned number.`,
556602
500,

0 commit comments

Comments
 (0)