wip: extract routes
This commit is contained in:
parent
5379043a14
commit
270042d132
@ -34,22 +34,13 @@ import {
|
||||
} from "@/app/monitoring/metrics2";
|
||||
import { activityCache } from "@/app/presence/sessionCache";
|
||||
import { encryptBytes, encryptString } from "@/modules/encrypt";
|
||||
import { GitHubProfile } from "./types";
|
||||
import { Fastify, GitHubProfile } from "./types";
|
||||
import { uploadImage } from "@/storage/uploadImage";
|
||||
import { separateName } from "@/utils/separateName";
|
||||
import { getPublicUrl } from "@/storage/files";
|
||||
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
userId: string;
|
||||
startTime?: number;
|
||||
}
|
||||
interface FastifyInstance {
|
||||
authenticate: any;
|
||||
}
|
||||
}
|
||||
|
||||
import { registerAuthRoutes } from "./routes/authRoutes";
|
||||
import { registerPushRoutes } from "./routes/pushRoutes";
|
||||
import { registerSessionRoutes } from "./routes/sessionRoutes";
|
||||
|
||||
export async function startApi(eventRouter: EventRouter): Promise<{ app: FastifyInstance; io: Server }> {
|
||||
|
||||
@ -126,7 +117,6 @@ export async function startApi(eventRouter: EventRouter): Promise<{ app: Fastify
|
||||
|
||||
app.setValidatorCompiler(validatorCompiler);
|
||||
app.setSerializerCompiler(serializerCompiler);
|
||||
const typed = app.withTypeProvider<ZodTypeProvider>();
|
||||
|
||||
// Add metrics hooks
|
||||
app.addHook('onRequest', async (request, reply) => {
|
||||
@ -249,127 +239,12 @@ export async function startApi(eventRouter: EventRouter): Promise<{ app: Fastify
|
||||
}
|
||||
});
|
||||
|
||||
// Auth schema
|
||||
typed.post('/v1/auth', {
|
||||
schema: {
|
||||
body: z.object({
|
||||
publicKey: z.string(),
|
||||
challenge: z.string(),
|
||||
signature: z.string()
|
||||
})
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const tweetnacl = (await import("tweetnacl")).default;
|
||||
const publicKey = privacyKit.decodeBase64(request.body.publicKey);
|
||||
const challenge = privacyKit.decodeBase64(request.body.challenge);
|
||||
const signature = privacyKit.decodeBase64(request.body.signature);
|
||||
const isValid = tweetnacl.sign.detached.verify(challenge, signature, publicKey);
|
||||
if (!isValid) {
|
||||
return reply.code(401).send({ error: 'Invalid signature' });
|
||||
}
|
||||
const typed = app.withTypeProvider<ZodTypeProvider>() as unknown as Fastify;
|
||||
|
||||
// Create or update user in database
|
||||
const publicKeyHex = privacyKit.encodeHex(publicKey);
|
||||
const user = await db.account.upsert({
|
||||
where: { publicKey: publicKeyHex },
|
||||
update: { updatedAt: new Date() },
|
||||
create: { publicKey: publicKeyHex }
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
token: await auth.createToken(user.id)
|
||||
});
|
||||
});
|
||||
|
||||
typed.post('/v1/auth/request', {
|
||||
schema: {
|
||||
body: z.object({
|
||||
publicKey: z.string(),
|
||||
}),
|
||||
response: {
|
||||
200: z.union([z.object({
|
||||
state: z.literal('requested'),
|
||||
}), z.object({
|
||||
state: z.literal('authorized'),
|
||||
token: z.string(),
|
||||
response: z.string()
|
||||
})]),
|
||||
401: z.object({
|
||||
error: z.literal('Invalid public key')
|
||||
})
|
||||
}
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const tweetnacl = (await import("tweetnacl")).default;
|
||||
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 publicKeyHex = privacyKit.encodeHex(publicKey);
|
||||
log({ module: 'auth-request' }, `Terminal auth request - publicKey hex: ${publicKeyHex}`);
|
||||
|
||||
const answer = await db.terminalAuthRequest.upsert({
|
||||
where: { publicKey: publicKeyHex },
|
||||
update: {},
|
||||
create: { publicKey: publicKeyHex }
|
||||
});
|
||||
|
||||
if (answer.response && answer.responseAccountId) {
|
||||
const token = await auth.createToken(answer.responseAccountId!, { session: answer.id });
|
||||
return reply.send({
|
||||
state: 'authorized',
|
||||
token: token,
|
||||
response: answer.response
|
||||
});
|
||||
}
|
||||
|
||||
return reply.send({ state: 'requested' });
|
||||
});
|
||||
|
||||
// Approve auth request
|
||||
typed.post('/v1/auth/response', {
|
||||
preHandler: app.authenticate,
|
||||
schema: {
|
||||
body: z.object({
|
||||
response: z.string(),
|
||||
publicKey: z.string()
|
||||
})
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
log({ module: 'auth-response' }, `Auth response endpoint hit - user: ${request.userId}, publicKey: ${request.body.publicKey.substring(0, 20)}...`);
|
||||
const tweetnacl = (await import("tweetnacl")).default;
|
||||
const publicKey = privacyKit.decodeBase64(request.body.publicKey);
|
||||
const isValid = tweetnacl.box.publicKeyLength === publicKey.length;
|
||||
if (!isValid) {
|
||||
log({ module: 'auth-response' }, `Invalid public key length: ${publicKey.length}`);
|
||||
return reply.code(401).send({ error: 'Invalid public key' });
|
||||
}
|
||||
const publicKeyHex = privacyKit.encodeHex(publicKey);
|
||||
log({ module: 'auth-response' }, `Looking for auth request with publicKey hex: ${publicKeyHex}`);
|
||||
const authRequest = await db.terminalAuthRequest.findUnique({
|
||||
where: { publicKey: publicKeyHex }
|
||||
});
|
||||
if (!authRequest) {
|
||||
log({ module: 'auth-response' }, `Auth request not found for publicKey: ${publicKeyHex}`);
|
||||
// Let's also check what auth requests exist
|
||||
const allRequests = await db.terminalAuthRequest.findMany({
|
||||
take: 5,
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
log({ module: 'auth-response' }, `Recent auth requests in DB: ${JSON.stringify(allRequests.map(r => ({ id: r.id, publicKey: r.publicKey.substring(0, 20) + '...', hasResponse: !!r.response })))}`);
|
||||
return reply.code(404).send({ error: 'Request not found' });
|
||||
}
|
||||
if (!authRequest.response) {
|
||||
await db.terminalAuthRequest.update({
|
||||
where: { id: authRequest.id },
|
||||
data: { response: request.body.response, responseAccountId: request.userId }
|
||||
});
|
||||
}
|
||||
return reply.send({ success: true });
|
||||
});
|
||||
// Routes
|
||||
registerAuthRoutes(typed);
|
||||
registerPushRoutes(typed);
|
||||
registerSessionRoutes(typed, eventRouter);
|
||||
|
||||
// GitHub OAuth parameters
|
||||
typed.get('/v1/connect/github/params', {
|
||||
@ -683,532 +558,6 @@ export async function startApi(eventRouter: EventRouter): Promise<{ app: Fastify
|
||||
}
|
||||
});
|
||||
|
||||
// Account auth request
|
||||
typed.post('/v1/auth/account/request', {
|
||||
schema: {
|
||||
body: z.object({
|
||||
publicKey: z.string(),
|
||||
}),
|
||||
response: {
|
||||
200: z.union([z.object({
|
||||
state: z.literal('requested'),
|
||||
}), z.object({
|
||||
state: z.literal('authorized'),
|
||||
token: z.string(),
|
||||
response: z.string()
|
||||
})]),
|
||||
401: z.object({
|
||||
error: z.literal('Invalid public key')
|
||||
})
|
||||
}
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const tweetnacl = (await import("tweetnacl")).default;
|
||||
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 answer = await db.accountAuthRequest.upsert({
|
||||
where: { publicKey: privacyKit.encodeHex(publicKey) },
|
||||
update: {},
|
||||
create: { publicKey: privacyKit.encodeHex(publicKey) }
|
||||
});
|
||||
|
||||
if (answer.response && answer.responseAccountId) {
|
||||
const token = await auth.createToken(answer.responseAccountId!);
|
||||
return reply.send({
|
||||
state: 'authorized',
|
||||
token: token,
|
||||
response: answer.response
|
||||
});
|
||||
}
|
||||
|
||||
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 tweetnacl = (await import("tweetnacl")).default;
|
||||
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.userId }
|
||||
});
|
||||
}
|
||||
return reply.send({ success: true });
|
||||
});
|
||||
|
||||
// OpenAI Realtime ephemeral token generation
|
||||
typed.post('/v1/openai/realtime-token', {
|
||||
preHandler: app.authenticate,
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
token: z.string()
|
||||
}),
|
||||
500: z.object({
|
||||
error: z.string()
|
||||
})
|
||||
}
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
try {
|
||||
// Check if OpenAI API key is configured on server
|
||||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
if (!OPENAI_API_KEY) {
|
||||
return reply.code(500).send({
|
||||
error: 'OpenAI API key not configured on server'
|
||||
});
|
||||
}
|
||||
|
||||
// Generate ephemeral token from OpenAI
|
||||
const response = await fetch('https://api.openai.com/v1/realtime/sessions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'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;
|
||||
};
|
||||
|
||||
return reply.send({
|
||||
token: data.client_secret.value
|
||||
});
|
||||
} catch (error) {
|
||||
log({ module: 'openai', level: 'error' }, 'Failed to generate ephemeral token', error);
|
||||
return reply.code(500).send({
|
||||
error: 'Failed to generate ephemeral token'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sessions API
|
||||
typed.get('/v1/sessions', {
|
||||
preHandler: app.authenticate,
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
|
||||
const sessions = await db.session.findMany({
|
||||
where: { accountId: userId },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: 150,
|
||||
select: {
|
||||
id: true,
|
||||
seq: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
metadata: true,
|
||||
metadataVersion: true,
|
||||
agentState: true,
|
||||
agentStateVersion: true,
|
||||
active: true,
|
||||
lastActiveAt: true,
|
||||
// messages: {
|
||||
// orderBy: { seq: 'desc' },
|
||||
// take: 1,
|
||||
// select: {
|
||||
// id: true,
|
||||
// seq: true,
|
||||
// content: true,
|
||||
// localId: true,
|
||||
// createdAt: true
|
||||
// }
|
||||
// }
|
||||
}
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
sessions: sessions.map((v) => {
|
||||
// const lastMessage = v.messages[0];
|
||||
const sessionUpdatedAt = v.updatedAt.getTime();
|
||||
// const lastMessageCreatedAt = lastMessage ? lastMessage.createdAt.getTime() : 0;
|
||||
|
||||
return {
|
||||
id: v.id,
|
||||
seq: v.seq,
|
||||
createdAt: v.createdAt.getTime(),
|
||||
updatedAt: sessionUpdatedAt,
|
||||
active: v.active,
|
||||
activeAt: v.lastActiveAt.getTime(),
|
||||
metadata: v.metadata,
|
||||
metadataVersion: v.metadataVersion,
|
||||
agentState: v.agentState,
|
||||
agentStateVersion: v.agentStateVersion,
|
||||
lastMessage: null
|
||||
};
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// V2 Sessions API - Active sessions only
|
||||
typed.get('/v2/sessions/active', {
|
||||
preHandler: app.authenticate,
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
limit: z.coerce.number().int().min(1).max(500).default(150)
|
||||
}).optional()
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
const limit = request.query?.limit || 150;
|
||||
|
||||
const sessions = await db.session.findMany({
|
||||
where: {
|
||||
accountId: userId,
|
||||
active: true,
|
||||
lastActiveAt: { gt: new Date(Date.now() - 1000 * 60 * 15) /* 15 minutes */ }
|
||||
},
|
||||
orderBy: { lastActiveAt: 'desc' },
|
||||
take: limit,
|
||||
select: {
|
||||
id: true,
|
||||
seq: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
metadata: true,
|
||||
metadataVersion: true,
|
||||
agentState: true,
|
||||
agentStateVersion: true,
|
||||
active: true,
|
||||
lastActiveAt: true,
|
||||
}
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
sessions: sessions.map((v) => ({
|
||||
id: v.id,
|
||||
seq: v.seq,
|
||||
createdAt: v.createdAt.getTime(),
|
||||
updatedAt: v.updatedAt.getTime(),
|
||||
active: v.active,
|
||||
activeAt: v.lastActiveAt.getTime(),
|
||||
metadata: v.metadata,
|
||||
metadataVersion: v.metadataVersion,
|
||||
agentState: v.agentState,
|
||||
agentStateVersion: v.agentStateVersion,
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
// V2 Sessions API - Cursor-based pagination with change tracking
|
||||
typed.get('/v2/sessions', {
|
||||
preHandler: app.authenticate,
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
cursor: z.string().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(200).default(50),
|
||||
changedSince: z.coerce.number().int().positive().optional()
|
||||
}).optional()
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
const { cursor, limit = 50, changedSince } = request.query || {};
|
||||
|
||||
// Decode cursor - simple ID-based cursor
|
||||
let cursorSessionId: string | undefined;
|
||||
if (cursor) {
|
||||
if (cursor.startsWith('cursor_v1_')) {
|
||||
cursorSessionId = cursor.substring(10);
|
||||
} else {
|
||||
return reply.code(400).send({ error: 'Invalid cursor format' });
|
||||
}
|
||||
}
|
||||
|
||||
// Build where clause
|
||||
const where: Prisma.SessionWhereInput = { accountId: userId };
|
||||
|
||||
// Add changedSince filter (just a filter, doesn't affect pagination)
|
||||
if (changedSince) {
|
||||
where.updatedAt = {
|
||||
gt: new Date(changedSince)
|
||||
};
|
||||
}
|
||||
|
||||
// Add cursor pagination - always by ID descending (most recent first)
|
||||
if (cursorSessionId) {
|
||||
where.id = {
|
||||
lt: cursorSessionId // Get sessions with ID less than cursor (for desc order)
|
||||
};
|
||||
}
|
||||
|
||||
// Always sort by ID descending for consistent pagination
|
||||
const orderBy = { id: 'desc' as const };
|
||||
|
||||
const sessions = await db.session.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
take: limit + 1, // Fetch one extra to determine if there are more
|
||||
select: {
|
||||
id: true,
|
||||
seq: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
metadata: true,
|
||||
metadataVersion: true,
|
||||
agentState: true,
|
||||
agentStateVersion: true,
|
||||
active: true,
|
||||
lastActiveAt: true,
|
||||
}
|
||||
});
|
||||
|
||||
// Check if there are more results
|
||||
const hasNext = sessions.length > limit;
|
||||
const resultSessions = hasNext ? sessions.slice(0, limit) : sessions;
|
||||
|
||||
// Generate next cursor - simple ID-based cursor
|
||||
let nextCursor: string | null = null;
|
||||
if (hasNext && resultSessions.length > 0) {
|
||||
const lastSession = resultSessions[resultSessions.length - 1];
|
||||
nextCursor = `cursor_v1_${lastSession.id}`;
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
sessions: resultSessions.map((v) => ({
|
||||
id: v.id,
|
||||
seq: v.seq,
|
||||
createdAt: v.createdAt.getTime(),
|
||||
updatedAt: v.updatedAt.getTime(),
|
||||
active: v.active,
|
||||
activeAt: v.lastActiveAt.getTime(),
|
||||
metadata: v.metadata,
|
||||
metadataVersion: v.metadataVersion,
|
||||
agentState: v.agentState,
|
||||
agentStateVersion: v.agentStateVersion,
|
||||
})),
|
||||
nextCursor,
|
||||
hasNext
|
||||
});
|
||||
});
|
||||
|
||||
// Create or load session by tag
|
||||
typed.post('/v1/sessions', {
|
||||
schema: {
|
||||
body: z.object({
|
||||
tag: z.string(),
|
||||
metadata: z.string(),
|
||||
agentState: z.string().nullish()
|
||||
})
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
const { tag, metadata } = request.body;
|
||||
|
||||
const session = await db.session.findFirst({
|
||||
where: {
|
||||
accountId: userId,
|
||||
tag: tag
|
||||
}
|
||||
});
|
||||
if (session) {
|
||||
logger.info({ module: 'session-create', sessionId: session.id, userId, tag }, `Found existing session: ${session.id} for tag ${tag}`);
|
||||
return reply.send({
|
||||
session: {
|
||||
id: session.id,
|
||||
seq: session.seq,
|
||||
metadata: session.metadata,
|
||||
metadataVersion: session.metadataVersion,
|
||||
agentState: session.agentState,
|
||||
agentStateVersion: session.agentStateVersion,
|
||||
active: session.active,
|
||||
activeAt: session.lastActiveAt.getTime(),
|
||||
createdAt: session.createdAt.getTime(),
|
||||
updatedAt: session.updatedAt.getTime(),
|
||||
lastMessage: null
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
// Resolve seq
|
||||
const updSeq = await allocateUserSeq(userId);
|
||||
|
||||
// Create session
|
||||
logger.info({ module: 'session-create', userId, tag }, `Creating new session for user ${userId} with tag ${tag}`);
|
||||
const session = await db.session.create({
|
||||
data: {
|
||||
accountId: userId,
|
||||
tag: tag,
|
||||
metadata: metadata
|
||||
}
|
||||
});
|
||||
logger.info({ module: 'session-create', sessionId: session.id, userId }, `Session created: ${session.id}`);
|
||||
|
||||
// Emit new session update
|
||||
const updatePayload = buildNewSessionUpdate(session, updSeq, randomKeyNaked(12));
|
||||
logger.info({
|
||||
module: 'session-create',
|
||||
userId,
|
||||
sessionId: session.id,
|
||||
updateType: 'new-session',
|
||||
updatePayload: JSON.stringify(updatePayload)
|
||||
}, `Emitting new-session update to all user connections`);
|
||||
eventRouter.emitUpdate({
|
||||
userId,
|
||||
payload: updatePayload,
|
||||
recipientFilter: { type: 'all-user-authenticated-connections' }
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
session: {
|
||||
id: session.id,
|
||||
seq: session.seq,
|
||||
metadata: session.metadata,
|
||||
metadataVersion: session.metadataVersion,
|
||||
agentState: session.agentState,
|
||||
agentStateVersion: session.agentStateVersion,
|
||||
active: session.active,
|
||||
activeAt: session.lastActiveAt.getTime(),
|
||||
createdAt: session.createdAt.getTime(),
|
||||
updatedAt: session.updatedAt.getTime(),
|
||||
lastMessage: null
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Push Token Registration API
|
||||
typed.post('/v1/push-tokens', {
|
||||
schema: {
|
||||
body: z.object({
|
||||
token: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
success: z.literal(true)
|
||||
}),
|
||||
500: z.object({
|
||||
error: z.literal('Failed to register push token')
|
||||
})
|
||||
}
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
const { token } = request.body;
|
||||
|
||||
try {
|
||||
await db.accountPushToken.upsert({
|
||||
where: {
|
||||
accountId_token: {
|
||||
accountId: userId,
|
||||
token: token
|
||||
}
|
||||
},
|
||||
update: {
|
||||
updatedAt: new Date()
|
||||
},
|
||||
create: {
|
||||
accountId: userId,
|
||||
token: token
|
||||
}
|
||||
});
|
||||
|
||||
return reply.send({ success: true });
|
||||
} catch (error) {
|
||||
return reply.code(500).send({ error: 'Failed to register push token' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete Push Token API
|
||||
typed.delete('/v1/push-tokens/:token', {
|
||||
schema: {
|
||||
params: z.object({
|
||||
token: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
success: z.literal(true)
|
||||
}),
|
||||
500: z.object({
|
||||
error: z.literal('Failed to delete push token')
|
||||
})
|
||||
}
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
const { token } = request.params;
|
||||
|
||||
try {
|
||||
await db.accountPushToken.deleteMany({
|
||||
where: {
|
||||
accountId: userId,
|
||||
token: token
|
||||
}
|
||||
});
|
||||
|
||||
return reply.send({ success: true });
|
||||
} catch (error) {
|
||||
return reply.code(500).send({ error: 'Failed to delete push token' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get Push Tokens API
|
||||
typed.get('/v1/push-tokens', {
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
|
||||
try {
|
||||
const tokens = await db.accountPushToken.findMany({
|
||||
where: {
|
||||
accountId: userId
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
tokens: tokens.map(t => ({
|
||||
id: t.id,
|
||||
token: t.token,
|
||||
createdAt: t.createdAt.getTime(),
|
||||
updatedAt: t.updatedAt.getTime()
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({ error: 'Failed to get push tokens' });
|
||||
}
|
||||
});
|
||||
|
||||
typed.get('/v1/account/profile', {
|
||||
preHandler: app.authenticate,
|
||||
}, async (request, reply) => {
|
||||
|
206
sources/app/api/routes/authRoutes.ts
Normal file
206
sources/app/api/routes/authRoutes.ts
Normal file
@ -0,0 +1,206 @@
|
||||
import { z } from "zod";
|
||||
import { type Fastify } from "../types";
|
||||
import * as privacyKit from "privacy-kit";
|
||||
import { db } from "@/storage/db";
|
||||
import { auth } from "@/app/auth/auth";
|
||||
import { log } from "@/utils/log";
|
||||
|
||||
export function registerAuthRoutes(app: Fastify) {
|
||||
app.post('/v1/auth', {
|
||||
schema: {
|
||||
body: z.object({
|
||||
publicKey: z.string(),
|
||||
challenge: z.string(),
|
||||
signature: z.string()
|
||||
})
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const tweetnacl = (await import("tweetnacl")).default;
|
||||
const publicKey = privacyKit.decodeBase64(request.body.publicKey);
|
||||
const challenge = privacyKit.decodeBase64(request.body.challenge);
|
||||
const signature = privacyKit.decodeBase64(request.body.signature);
|
||||
const isValid = tweetnacl.sign.detached.verify(challenge, signature, publicKey);
|
||||
if (!isValid) {
|
||||
return reply.code(401).send({ error: 'Invalid signature' });
|
||||
}
|
||||
|
||||
// Create or update user in database
|
||||
const publicKeyHex = privacyKit.encodeHex(publicKey);
|
||||
const user = await db.account.upsert({
|
||||
where: { publicKey: publicKeyHex },
|
||||
update: { updatedAt: new Date() },
|
||||
create: { publicKey: publicKeyHex }
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
token: await auth.createToken(user.id)
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/v1/auth/request', {
|
||||
schema: {
|
||||
body: z.object({
|
||||
publicKey: z.string(),
|
||||
}),
|
||||
response: {
|
||||
200: z.union([z.object({
|
||||
state: z.literal('requested'),
|
||||
}), z.object({
|
||||
state: z.literal('authorized'),
|
||||
token: z.string(),
|
||||
response: z.string()
|
||||
})]),
|
||||
401: z.object({
|
||||
error: z.literal('Invalid public key')
|
||||
})
|
||||
}
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const tweetnacl = (await import("tweetnacl")).default;
|
||||
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 publicKeyHex = privacyKit.encodeHex(publicKey);
|
||||
log({ module: 'auth-request' }, `Terminal auth request - publicKey hex: ${publicKeyHex}`);
|
||||
|
||||
const answer = await db.terminalAuthRequest.upsert({
|
||||
where: { publicKey: publicKeyHex },
|
||||
update: {},
|
||||
create: { publicKey: publicKeyHex }
|
||||
});
|
||||
|
||||
if (answer.response && answer.responseAccountId) {
|
||||
const token = await auth.createToken(answer.responseAccountId!, { session: answer.id });
|
||||
return reply.send({
|
||||
state: 'authorized',
|
||||
token: token,
|
||||
response: answer.response
|
||||
});
|
||||
}
|
||||
|
||||
return reply.send({ state: 'requested' });
|
||||
});
|
||||
|
||||
// Approve auth request
|
||||
app.post('/v1/auth/response', {
|
||||
preHandler: app.authenticate,
|
||||
schema: {
|
||||
body: z.object({
|
||||
response: z.string(),
|
||||
publicKey: z.string()
|
||||
})
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
log({ module: 'auth-response' }, `Auth response endpoint hit - user: ${request.userId}, publicKey: ${request.body.publicKey.substring(0, 20)}...`);
|
||||
const tweetnacl = (await import("tweetnacl")).default;
|
||||
const publicKey = privacyKit.decodeBase64(request.body.publicKey);
|
||||
const isValid = tweetnacl.box.publicKeyLength === publicKey.length;
|
||||
if (!isValid) {
|
||||
log({ module: 'auth-response' }, `Invalid public key length: ${publicKey.length}`);
|
||||
return reply.code(401).send({ error: 'Invalid public key' });
|
||||
}
|
||||
const publicKeyHex = privacyKit.encodeHex(publicKey);
|
||||
log({ module: 'auth-response' }, `Looking for auth request with publicKey hex: ${publicKeyHex}`);
|
||||
const authRequest = await db.terminalAuthRequest.findUnique({
|
||||
where: { publicKey: publicKeyHex }
|
||||
});
|
||||
if (!authRequest) {
|
||||
log({ module: 'auth-response' }, `Auth request not found for publicKey: ${publicKeyHex}`);
|
||||
// Let's also check what auth requests exist
|
||||
const allRequests = await db.terminalAuthRequest.findMany({
|
||||
take: 5,
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
log({ module: 'auth-response' }, `Recent auth requests in DB: ${JSON.stringify(allRequests.map(r => ({ id: r.id, publicKey: r.publicKey.substring(0, 20) + '...', hasResponse: !!r.response })))}`);
|
||||
return reply.code(404).send({ error: 'Request not found' });
|
||||
}
|
||||
if (!authRequest.response) {
|
||||
await db.terminalAuthRequest.update({
|
||||
where: { id: authRequest.id },
|
||||
data: { response: request.body.response, responseAccountId: request.userId }
|
||||
});
|
||||
}
|
||||
return reply.send({ success: true });
|
||||
});
|
||||
|
||||
// Account auth request
|
||||
app.post('/v1/auth/account/request', {
|
||||
schema: {
|
||||
body: z.object({
|
||||
publicKey: z.string(),
|
||||
}),
|
||||
response: {
|
||||
200: z.union([z.object({
|
||||
state: z.literal('requested'),
|
||||
}), z.object({
|
||||
state: z.literal('authorized'),
|
||||
token: z.string(),
|
||||
response: z.string()
|
||||
})]),
|
||||
401: z.object({
|
||||
error: z.literal('Invalid public key')
|
||||
})
|
||||
}
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const tweetnacl = (await import("tweetnacl")).default;
|
||||
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 answer = await db.accountAuthRequest.upsert({
|
||||
where: { publicKey: privacyKit.encodeHex(publicKey) },
|
||||
update: {},
|
||||
create: { publicKey: privacyKit.encodeHex(publicKey) }
|
||||
});
|
||||
|
||||
if (answer.response && answer.responseAccountId) {
|
||||
const token = await auth.createToken(answer.responseAccountId!);
|
||||
return reply.send({
|
||||
state: 'authorized',
|
||||
token: token,
|
||||
response: answer.response
|
||||
});
|
||||
}
|
||||
|
||||
return reply.send({ state: 'requested' });
|
||||
});
|
||||
|
||||
// Approve account auth request
|
||||
app.post('/v1/auth/account/response', {
|
||||
preHandler: app.authenticate,
|
||||
schema: {
|
||||
body: z.object({
|
||||
response: z.string(),
|
||||
publicKey: z.string()
|
||||
})
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const tweetnacl = (await import("tweetnacl")).default;
|
||||
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.userId }
|
||||
});
|
||||
}
|
||||
return reply.send({ success: true });
|
||||
});
|
||||
|
||||
}
|
112
sources/app/api/routes/pushRoutes.ts
Normal file
112
sources/app/api/routes/pushRoutes.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { z } from "zod";
|
||||
import { type Fastify } from "../types";
|
||||
import { db } from "@/storage/db";
|
||||
|
||||
export function registerPushRoutes(app: Fastify) {
|
||||
|
||||
// Push Token Registration API
|
||||
app.post('/v1/push-tokens', {
|
||||
schema: {
|
||||
body: z.object({
|
||||
token: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
success: z.literal(true)
|
||||
}),
|
||||
500: z.object({
|
||||
error: z.literal('Failed to register push token')
|
||||
})
|
||||
}
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
const { token } = request.body;
|
||||
|
||||
try {
|
||||
await db.accountPushToken.upsert({
|
||||
where: {
|
||||
accountId_token: {
|
||||
accountId: userId,
|
||||
token: token
|
||||
}
|
||||
},
|
||||
update: {
|
||||
updatedAt: new Date()
|
||||
},
|
||||
create: {
|
||||
accountId: userId,
|
||||
token: token
|
||||
}
|
||||
});
|
||||
|
||||
return reply.send({ success: true });
|
||||
} catch (error) {
|
||||
return reply.code(500).send({ error: 'Failed to register push token' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete Push Token API
|
||||
app.delete('/v1/push-tokens/:token', {
|
||||
schema: {
|
||||
params: z.object({
|
||||
token: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
success: z.literal(true)
|
||||
}),
|
||||
500: z.object({
|
||||
error: z.literal('Failed to delete push token')
|
||||
})
|
||||
}
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
const { token } = request.params;
|
||||
|
||||
try {
|
||||
await db.accountPushToken.deleteMany({
|
||||
where: {
|
||||
accountId: userId,
|
||||
token: token
|
||||
}
|
||||
});
|
||||
|
||||
return reply.send({ success: true });
|
||||
} catch (error) {
|
||||
return reply.code(500).send({ error: 'Failed to delete push token' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get Push Tokens API
|
||||
app.get('/v1/push-tokens', {
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
|
||||
try {
|
||||
const tokens = await db.accountPushToken.findMany({
|
||||
where: {
|
||||
accountId: userId
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
tokens: tokens.map(t => ({
|
||||
id: t.id,
|
||||
token: t.token,
|
||||
createdAt: t.createdAt.getTime(),
|
||||
updatedAt: t.updatedAt.getTime()
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({ error: 'Failed to get push tokens' });
|
||||
}
|
||||
});
|
||||
}
|
296
sources/app/api/routes/sessionRoutes.ts
Normal file
296
sources/app/api/routes/sessionRoutes.ts
Normal file
@ -0,0 +1,296 @@
|
||||
import { EventRouter, buildNewSessionUpdate } from "@/modules/eventRouter";
|
||||
import { type Fastify } from "../types";
|
||||
import { db } from "@/storage/db";
|
||||
import { z } from "zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { log } from "@/utils/log";
|
||||
import { randomKeyNaked } from "@/utils/randomKeyNaked";
|
||||
import { allocateUserSeq } from "@/storage/seq";
|
||||
|
||||
export function registerSessionRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
|
||||
// Sessions API
|
||||
app.get('/v1/sessions', {
|
||||
preHandler: app.authenticate,
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
|
||||
const sessions = await db.session.findMany({
|
||||
where: { accountId: userId },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: 150,
|
||||
select: {
|
||||
id: true,
|
||||
seq: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
metadata: true,
|
||||
metadataVersion: true,
|
||||
agentState: true,
|
||||
agentStateVersion: true,
|
||||
active: true,
|
||||
lastActiveAt: true,
|
||||
// messages: {
|
||||
// orderBy: { seq: 'desc' },
|
||||
// take: 1,
|
||||
// select: {
|
||||
// id: true,
|
||||
// seq: true,
|
||||
// content: true,
|
||||
// localId: true,
|
||||
// createdAt: true
|
||||
// }
|
||||
// }
|
||||
}
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
sessions: sessions.map((v) => {
|
||||
// const lastMessage = v.messages[0];
|
||||
const sessionUpdatedAt = v.updatedAt.getTime();
|
||||
// const lastMessageCreatedAt = lastMessage ? lastMessage.createdAt.getTime() : 0;
|
||||
|
||||
return {
|
||||
id: v.id,
|
||||
seq: v.seq,
|
||||
createdAt: v.createdAt.getTime(),
|
||||
updatedAt: sessionUpdatedAt,
|
||||
active: v.active,
|
||||
activeAt: v.lastActiveAt.getTime(),
|
||||
metadata: v.metadata,
|
||||
metadataVersion: v.metadataVersion,
|
||||
agentState: v.agentState,
|
||||
agentStateVersion: v.agentStateVersion,
|
||||
lastMessage: null
|
||||
};
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// V2 Sessions API - Active sessions only
|
||||
app.get('/v2/sessions/active', {
|
||||
preHandler: app.authenticate,
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
limit: z.coerce.number().int().min(1).max(500).default(150)
|
||||
}).optional()
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
const limit = request.query?.limit || 150;
|
||||
|
||||
const sessions = await db.session.findMany({
|
||||
where: {
|
||||
accountId: userId,
|
||||
active: true,
|
||||
lastActiveAt: { gt: new Date(Date.now() - 1000 * 60 * 15) /* 15 minutes */ }
|
||||
},
|
||||
orderBy: { lastActiveAt: 'desc' },
|
||||
take: limit,
|
||||
select: {
|
||||
id: true,
|
||||
seq: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
metadata: true,
|
||||
metadataVersion: true,
|
||||
agentState: true,
|
||||
agentStateVersion: true,
|
||||
active: true,
|
||||
lastActiveAt: true,
|
||||
}
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
sessions: sessions.map((v) => ({
|
||||
id: v.id,
|
||||
seq: v.seq,
|
||||
createdAt: v.createdAt.getTime(),
|
||||
updatedAt: v.updatedAt.getTime(),
|
||||
active: v.active,
|
||||
activeAt: v.lastActiveAt.getTime(),
|
||||
metadata: v.metadata,
|
||||
metadataVersion: v.metadataVersion,
|
||||
agentState: v.agentState,
|
||||
agentStateVersion: v.agentStateVersion,
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
// V2 Sessions API - Cursor-based pagination with change tracking
|
||||
app.get('/v2/sessions', {
|
||||
preHandler: app.authenticate,
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
cursor: z.string().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(200).default(50),
|
||||
changedSince: z.coerce.number().int().positive().optional()
|
||||
}).optional()
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
const { cursor, limit = 50, changedSince } = request.query || {};
|
||||
|
||||
// Decode cursor - simple ID-based cursor
|
||||
let cursorSessionId: string | undefined;
|
||||
if (cursor) {
|
||||
if (cursor.startsWith('cursor_v1_')) {
|
||||
cursorSessionId = cursor.substring(10);
|
||||
} else {
|
||||
return reply.code(400).send({ error: 'Invalid cursor format' });
|
||||
}
|
||||
}
|
||||
|
||||
// Build where clause
|
||||
const where: Prisma.SessionWhereInput = { accountId: userId };
|
||||
|
||||
// Add changedSince filter (just a filter, doesn't affect pagination)
|
||||
if (changedSince) {
|
||||
where.updatedAt = {
|
||||
gt: new Date(changedSince)
|
||||
};
|
||||
}
|
||||
|
||||
// Add cursor pagination - always by ID descending (most recent first)
|
||||
if (cursorSessionId) {
|
||||
where.id = {
|
||||
lt: cursorSessionId // Get sessions with ID less than cursor (for desc order)
|
||||
};
|
||||
}
|
||||
|
||||
// Always sort by ID descending for consistent pagination
|
||||
const orderBy = { id: 'desc' as const };
|
||||
|
||||
const sessions = await db.session.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
take: limit + 1, // Fetch one extra to determine if there are more
|
||||
select: {
|
||||
id: true,
|
||||
seq: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
metadata: true,
|
||||
metadataVersion: true,
|
||||
agentState: true,
|
||||
agentStateVersion: true,
|
||||
active: true,
|
||||
lastActiveAt: true,
|
||||
}
|
||||
});
|
||||
|
||||
// Check if there are more results
|
||||
const hasNext = sessions.length > limit;
|
||||
const resultSessions = hasNext ? sessions.slice(0, limit) : sessions;
|
||||
|
||||
// Generate next cursor - simple ID-based cursor
|
||||
let nextCursor: string | null = null;
|
||||
if (hasNext && resultSessions.length > 0) {
|
||||
const lastSession = resultSessions[resultSessions.length - 1];
|
||||
nextCursor = `cursor_v1_${lastSession.id}`;
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
sessions: resultSessions.map((v) => ({
|
||||
id: v.id,
|
||||
seq: v.seq,
|
||||
createdAt: v.createdAt.getTime(),
|
||||
updatedAt: v.updatedAt.getTime(),
|
||||
active: v.active,
|
||||
activeAt: v.lastActiveAt.getTime(),
|
||||
metadata: v.metadata,
|
||||
metadataVersion: v.metadataVersion,
|
||||
agentState: v.agentState,
|
||||
agentStateVersion: v.agentStateVersion,
|
||||
})),
|
||||
nextCursor,
|
||||
hasNext
|
||||
});
|
||||
});
|
||||
|
||||
// Create or load session by tag
|
||||
app.post('/v1/sessions', {
|
||||
schema: {
|
||||
body: z.object({
|
||||
tag: z.string(),
|
||||
metadata: z.string(),
|
||||
agentState: z.string().nullish()
|
||||
})
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
}, async (request, reply) => {
|
||||
const userId = request.userId;
|
||||
const { tag, metadata } = request.body;
|
||||
|
||||
const session = await db.session.findFirst({
|
||||
where: {
|
||||
accountId: userId,
|
||||
tag: tag
|
||||
}
|
||||
});
|
||||
if (session) {
|
||||
log({ module: 'session-create', sessionId: session.id, userId, tag }, `Found existing session: ${session.id} for tag ${tag}`);
|
||||
return reply.send({
|
||||
session: {
|
||||
id: session.id,
|
||||
seq: session.seq,
|
||||
metadata: session.metadata,
|
||||
metadataVersion: session.metadataVersion,
|
||||
agentState: session.agentState,
|
||||
agentStateVersion: session.agentStateVersion,
|
||||
active: session.active,
|
||||
activeAt: session.lastActiveAt.getTime(),
|
||||
createdAt: session.createdAt.getTime(),
|
||||
updatedAt: session.updatedAt.getTime(),
|
||||
lastMessage: null
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
// Resolve seq
|
||||
const updSeq = await allocateUserSeq(userId);
|
||||
|
||||
// Create session
|
||||
log({ module: 'session-create', userId, tag }, `Creating new session for user ${userId} with tag ${tag}`);
|
||||
const session = await db.session.create({
|
||||
data: {
|
||||
accountId: userId,
|
||||
tag: tag,
|
||||
metadata: metadata
|
||||
}
|
||||
});
|
||||
log({ module: 'session-create', sessionId: session.id, userId }, `Session created: ${session.id}`);
|
||||
|
||||
// Emit new session update
|
||||
const updatePayload = buildNewSessionUpdate(session, updSeq, randomKeyNaked(12));
|
||||
log({
|
||||
module: 'session-create',
|
||||
userId,
|
||||
sessionId: session.id,
|
||||
updateType: 'new-session',
|
||||
updatePayload: JSON.stringify(updatePayload)
|
||||
}, `Emitting new-session update to all user connections`);
|
||||
eventRouter.emitUpdate({
|
||||
userId,
|
||||
payload: updatePayload,
|
||||
recipientFilter: { type: 'all-user-authenticated-connections' }
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
session: {
|
||||
id: session.id,
|
||||
seq: session.seq,
|
||||
metadata: session.metadata,
|
||||
metadataVersion: session.metadataVersion,
|
||||
agentState: session.agentState,
|
||||
agentStateVersion: session.agentStateVersion,
|
||||
active: session.active,
|
||||
activeAt: session.lastActiveAt.getTime(),
|
||||
createdAt: session.createdAt.getTime(),
|
||||
updatedAt: session.updatedAt.getTime(),
|
||||
lastMessage: null
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
@ -39,7 +39,7 @@ export interface GitHubProfile {
|
||||
}
|
||||
|
||||
export interface GitHubOrg {
|
||||
|
||||
|
||||
}
|
||||
|
||||
export type Fastify = FastifyInstance<
|
||||
@ -48,4 +48,14 @@ export type Fastify = FastifyInstance<
|
||||
ServerResponse<IncomingMessage>,
|
||||
FastifyBaseLogger,
|
||||
ZodTypeProvider
|
||||
>;
|
||||
>;
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
userId: string;
|
||||
startTime?: number;
|
||||
}
|
||||
interface FastifyInstance {
|
||||
authenticate: any;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user