feat: add app-to-app authentication

This commit is contained in:
Steve Korshakov 2025-08-16 10:13:44 -07:00 committed by Kirill Dubovitskiy
parent 62a2280268
commit 6f1aefc056
3 changed files with 89 additions and 46 deletions

View File

@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "AccountAuthRequest" (
"id" TEXT NOT NULL,
"publicKey" TEXT NOT NULL,
"response" TEXT,
"responseAccountId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AccountAuthRequest_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "AccountAuthRequest_publicKey_key" ON "AccountAuthRequest"("publicKey");
-- AddForeignKey
ALTER TABLE "AccountAuthRequest" ADD CONSTRAINT "AccountAuthRequest_responseAccountId_fkey" FOREIGN KEY ("responseAccountId") REFERENCES "Account"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -29,6 +29,7 @@ model Account {
Session Session[] Session Session[]
AccountPushToken AccountPushToken[] AccountPushToken AccountPushToken[]
TerminalAuthRequest TerminalAuthRequest[] TerminalAuthRequest TerminalAuthRequest[]
AccountAuthRequest AccountAuthRequest[]
UsageReport UsageReport[] UsageReport UsageReport[]
Machine Machine[] Machine Machine[]
} }
@ -43,6 +44,16 @@ model TerminalAuthRequest {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model AccountAuthRequest {
id String @id @default(cuid())
publicKey String @unique
response String?
responseAccountId String?
responseAccount Account? @relation(fields: [responseAccountId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model AccountPushToken { model AccountPushToken {
id String @id @default(cuid()) id String @id @default(cuid())
accountId String accountId String

View File

@ -292,63 +292,78 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
return reply.send({ success: true }); return reply.send({ success: true });
}); });
// OpenAI Realtime ephemeral token generation // Account auth request
typed.post('/v1/openai/realtime-token', { typed.post('/v1/auth/account/request', {
preHandler: app.authenticate,
schema: { schema: {
body: z.object({
publicKey: z.string(),
}),
response: { response: {
200: z.object({ 200: z.union([z.object({
token: z.string() state: z.literal('requested'),
}), }), z.object({
500: z.object({ state: z.literal('authorized'),
error: z.string() token: z.string(),
response: z.string()
})]),
401: z.object({
error: z.literal('Invalid public key')
}) })
} }
} }
}, async (request, reply) => { }, async (request, reply) => {
try { const publicKey = privacyKit.decodeBase64(request.body.publicKey);
// Check if OpenAI API key is configured on server const isValid = tweetnacl.box.publicKeyLength === publicKey.length;
const OPENAI_API_KEY = process.env.OPENAI_API_KEY; if (!isValid) {
if (!OPENAI_API_KEY) { return reply.code(401).send({ error: 'Invalid public key' });
return reply.code(500).send({ }
error: 'OpenAI API key not configured on server'
});
}
// Generate ephemeral token from OpenAI const answer = await db.accountAuthRequest.upsert({
const response = await fetch('https://api.openai.com/v1/realtime/sessions', { where: { publicKey: privacyKit.encodeHex(publicKey) },
method: 'POST', update: {},
headers: { create: { publicKey: privacyKit.encodeHex(publicKey) }
'Authorization': `Bearer ${OPENAI_API_KEY}`, });
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o-realtime-preview-2024-12-17',
voice: 'verse',
}),
});
if (!response.ok) {
throw new Error(`OpenAI API error: ${response.status}`);
}
const data = await response.json() as {
client_secret: {
value: string;
expires_at: number;
};
id: string;
};
if (answer.response && answer.responseAccountId) {
const token = await tokenGenerator.new({ user: answer.responseAccountId! });
return reply.send({ return reply.send({
token: data.client_secret.value state: 'authorized',
}); token: token,
} catch (error) { response: answer.response
log({ module: 'openai', level: 'error' }, 'Failed to generate ephemeral token', error);
return reply.code(500).send({
error: 'Failed to generate ephemeral token'
}); });
} }
return reply.send({ state: 'requested' });
});
// Approve account auth request
typed.post('/v1/auth/account/response', {
preHandler: app.authenticate,
schema: {
body: z.object({
response: z.string(),
publicKey: z.string()
})
}
}, async (request, reply) => {
const publicKey = privacyKit.decodeBase64(request.body.publicKey);
const isValid = tweetnacl.box.publicKeyLength === publicKey.length;
if (!isValid) {
return reply.code(401).send({ error: 'Invalid public key' });
}
const authRequest = await db.accountAuthRequest.findUnique({
where: { publicKey: privacyKit.encodeHex(publicKey) }
});
if (!authRequest) {
return reply.code(404).send({ error: 'Request not found' });
}
if (!authRequest.response) {
await db.accountAuthRequest.update({
where: { id: authRequest.id },
data: { response: request.body.response, responseAccountId: request.user.id }
});
}
return reply.send({ success: true });
}); });
// Sessions API // Sessions API