ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
1import { AuthenticatedHandler } from './core/types';
2import type { ExtensionImportRequest, ExtensionImportResponse } from '@atlast/shared';
3import { z } from 'zod';
4import crypto from 'crypto';
5import { withAuthErrorHandling } from './core/middleware';
6import { ValidationError } from './core/errors';
7import { UploadRepository, SourceAccountRepository } from './repositories';
8import { normalize } from './utils/string.utils';
9import { successResponse } from './utils';
10
11/**
12 * Validation schema for extension import request
13 */
14const ExtensionImportSchema = z.object({
15 platform: z.string(),
16 usernames: z.array(z.string()).min(1).max(10000),
17 metadata: z.object({
18 extensionVersion: z.string(),
19 scrapedAt: z.string(),
20 pageType: z.enum(['following', 'followers', 'list']),
21 sourceUrl: z.string().url()
22 })
23});
24
25/**
26 * Extension import endpoint
27 * POST /extension-import
28 *
29 * Requires authentication. Creates upload and saves usernames immediately.
30 */
31const extensionImportHandler: AuthenticatedHandler = async (context) => {
32 const body: ExtensionImportRequest = JSON.parse(context.event.body || '{}');
33
34 // Validate request
35 const validatedData = ExtensionImportSchema.parse(body);
36
37 console.log('[extension-import] Received import:', {
38 did: context.did,
39 platform: validatedData.platform,
40 usernameCount: validatedData.usernames.length,
41 pageType: validatedData.metadata.pageType,
42 extensionVersion: validatedData.metadata.extensionVersion
43 });
44
45 // Generate upload ID
46 const uploadId = crypto.randomBytes(16).toString('hex');
47
48 // Create upload and save source accounts
49 const uploadRepo = new UploadRepository();
50 const sourceAccountRepo = new SourceAccountRepository();
51
52 // Create upload record
53 await uploadRepo.createUpload(
54 uploadId,
55 context.did,
56 validatedData.platform,
57 validatedData.usernames.length,
58 0 // matchedUsers - will be updated after search
59 );
60
61 console.log(`[extension-import] Created upload ${uploadId} for user ${context.did}`);
62
63 // Save source accounts using bulk insert and link to upload
64 try {
65 const sourceAccountIdMap = await sourceAccountRepo.bulkCreate(
66 validatedData.platform,
67 validatedData.usernames
68 );
69 console.log(`[extension-import] Saved ${validatedData.usernames.length} source accounts`);
70
71 // Link source accounts to this upload
72 const links = Array.from(sourceAccountIdMap.values()).map(sourceAccountId => ({
73 sourceAccountId,
74 sourceDate: validatedData.metadata.scrapedAt
75 }));
76
77 await sourceAccountRepo.linkUserToAccounts(uploadId, context.did, links);
78 console.log(`[extension-import] Linked ${links.length} source accounts to upload`);
79 } catch (error) {
80 console.error('[extension-import] Error saving source accounts:', error);
81 // Continue anyway - upload is created, frontend can still search
82 }
83
84 // Return upload data for frontend to search
85 const response: ExtensionImportResponse = {
86 importId: uploadId,
87 usernameCount: validatedData.usernames.length,
88 redirectUrl: `/?uploadId=${uploadId}` // Frontend will load results from uploadId param
89 };
90
91 return successResponse(response);
92};
93
94export const handler = withAuthErrorHandling(extensionImportHandler);