+35
-30
dist/index.html
+35
-30
dist/index.html
···
1
-
<!doctype html>
2
-
<html lang="en">
3
-
<head>
4
-
<meta charset="UTF-8" />
5
-
<link rel="manifest" href="/site.webmanifest" />
6
-
<link
7
-
rel="apple-touch-icon"
8
-
sizes="180x180"
9
-
href="/apple-touch-icon.png"
10
-
/>
11
-
<link
12
-
rel="icon"
13
-
type="image/x-icon"
14
-
sizes="32x32"
15
-
href="/favicon.ico"
16
-
/>
17
-
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
18
-
19
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
20
-
<title>
21
-
ATLast: Sync Your TikTok Follows → ATmosphere (Skylight, Bluesky,
22
-
etc.)
23
-
</title>
24
-
<script type="module" crossorigin src="/assets/index-DhUfpNfM.js"></script>
25
-
<link rel="stylesheet" crossorigin href="/assets/index-jFgtXSoO.css">
26
-
</head>
27
-
<body>
28
-
<div id="root"></div>
29
-
</body>
30
-
</html>
1
+
<!doctype html>
2
+
<html lang="en">
3
+
<head>
4
+
<meta charset="UTF-8" />
5
+
<link rel="manifest" href="/site.webmanifest" />
6
+
<link
7
+
rel="apple-touch-icon"
8
+
sizes="180x180"
9
+
href="/apple-touch-icon.png"
10
+
/>
11
+
<link
12
+
rel="icon"
13
+
type="image/x-icon"
14
+
sizes="32x32"
15
+
href="/favicon.ico"
16
+
/>
17
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
18
+
19
+
<meta
20
+
name="viewport"
21
+
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
22
+
/>
23
+
<meta name="apple-mobile-web-app-capable" content="yes" />
24
+
<meta
25
+
name="apple-mobile-web-app-status-bar-style"
26
+
content="black-translucent"
27
+
/>
28
+
<title>ATLast: Find Your People in the ATmosphere</title>
29
+
<script type="module" crossorigin src="/assets/index-D7a6vDuT.js"></script>
30
+
<link rel="stylesheet" crossorigin href="/assets/index-CIYGhL08.css">
31
+
</head>
32
+
<body>
33
+
<div id="root"></div>
34
+
</body>
35
+
</html>
+10
-5
index.html
+10
-5
index.html
···
16
16
/>
17
17
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
18
18
19
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
20
-
<title>
21
-
ATLast: Sync Your TikTok Follows → ATmosphere (Skylight, Bluesky,
22
-
etc.)
23
-
</title>
19
+
<meta
20
+
name="viewport"
21
+
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
22
+
/>
23
+
<meta name="apple-mobile-web-app-capable" content="yes" />
24
+
<meta
25
+
name="apple-mobile-web-app-status-bar-style"
26
+
content="black-translucent"
27
+
/>
28
+
<title>ATLast: Find Your People in the ATmosphere</title>
24
29
</head>
25
30
<body>
26
31
<div id="root"></div>
+7
-1
netlify/functions/core/middleware/error.middleware.ts
+7
-1
netlify/functions/core/middleware/error.middleware.ts
···
12
12
try {
13
13
return await handler(event);
14
14
} catch (error) {
15
-
console.error("Handler error:", error);
15
+
console.error(
16
+
"Handler error:",
17
+
error instanceof Error ? error.message : String(error),
18
+
);
19
+
if (error instanceof Error && error.stack) {
20
+
console.error("Stack trace:", error.stack);
21
+
}
16
22
17
23
if (error instanceof ApiError) {
18
24
return errorResponse(error.message, error.statusCode, error.details);
+30
-8
netlify/functions/infrastructure/oauth/config.ts
+30
-8
netlify/functions/infrastructure/oauth/config.ts
···
1
1
import { OAuthConfig } from "../../core/types";
2
-
import { ApiError } from "../../core/errors";
3
2
import { configCache } from "../cache/CacheService";
4
3
import { CONFIG } from "../../core/config/constants";
5
4
···
8
7
}): OAuthConfig {
9
8
// 1. Determine host dynamically
10
9
const host = event?.headers?.host;
11
-
const cacheKey = `oauth-config-${host || "default"}`;
10
+
console.log("[oauth-config] Host from headers:", host);
11
+
console.log("[oauth-config] All relevant headers:", {
12
+
host: event?.headers?.host,
13
+
"x-forwarded-host": event?.headers?.["x-forwarded-host"],
14
+
"x-forwarded-proto": event?.headers?.["x-forwarded-proto"],
15
+
"x-nf-deploy-context": event?.headers?.["x-nf-deploy-context"],
16
+
});
12
17
18
+
const cacheKey = `oauth-config-${host || "default"}`;
13
19
const cached = configCache.get(cacheKey) as OAuthConfig | undefined;
14
20
if (cached) {
15
21
return cached;
···
17
23
18
24
let baseUrl: string | undefined;
19
25
20
-
// 2. Determine if local based on host header
26
+
// 2. Check if we're in Netlify Live mode (--live)
27
+
// In --live mode, DEPLOY_URL will be the tunnel URL even though host header is localhost
28
+
const deployUrl = process.env.DEPLOY_URL || process.env.URL;
29
+
const isNetlifyLive = deployUrl?.includes(".netlify.live");
30
+
31
+
// 3. Determine if local based on host header AND not in live mode
21
32
const isLocal =
22
-
!host || host.includes("localhost") || host.includes("127.0.0.1");
33
+
!isNetlifyLive &&
34
+
(!host || host.includes("localhost") || host.includes("127.0.0.1"));
23
35
24
36
// 3. Local oauth config
25
37
if (isLocal) {
···
51
63
return config;
52
64
}
53
65
54
-
// 4. Production oauth config
66
+
// 4. Production + Live oauth config
55
67
console.log("Using confidential OAuth client for:", baseUrl);
56
68
57
69
const forwardedProto = event?.headers?.["x-forwarded-proto"] || "https";
58
-
baseUrl = host
59
-
? `${forwardedProto}://${host}`
60
-
: process.env.DEPLOY_URL || process.env.URL;
70
+
// If we're in Netlify Live mode, use the DEPLOY_URL (tunnel URL)
71
+
// Otherwise use the host header
72
+
if (isNetlifyLive) {
73
+
baseUrl = deployUrl;
74
+
console.log("Using Netlify Live tunnel for OAuth:", baseUrl);
75
+
} else {
76
+
baseUrl = host ? `${forwardedProto}://${host}` : deployUrl;
77
+
console.log("Using confidential OAuth client for:", baseUrl);
78
+
}
79
+
80
+
if (!baseUrl) {
81
+
throw new Error("No base URL available for OAuth configuration");
82
+
}
61
83
62
84
const config: OAuthConfig = {
63
85
clientId: `${baseUrl}/oauth-client-metadata.json`,
+20
-4
netlify/functions/oauth-callback.ts
+20
-4
netlify/functions/oauth-callback.ts
···
50
50
51
51
console.log("[oauth-callback] Created user session:", sessionId);
52
52
53
-
const cookieName = isDev ? "atlast_session_dev" : "atlast_session";
54
-
const cookieFlags = isDev
55
-
? `HttpOnly; SameSite=Lax; Max-Age=${CONFIG.COOKIE_MAX_AGE}; Path=/`
56
-
: `HttpOnly; SameSite=Lax; Max-Age=${CONFIG.COOKIE_MAX_AGE}; Path=/; Secure`;
53
+
// Determine cookie configuration
54
+
// Use DEPLOY_URL to detect Netlify Live mode
55
+
const isNetlifyLive = (process.env.DEPLOY_URL || process.env.URL)?.includes(
56
+
".netlify.live",
57
+
);
58
+
const isSecure = currentUrl.startsWith("https://") || isNetlifyLive;
59
+
60
+
// Use dev cookie for development, otherwise production cookie
61
+
const cookieName =
62
+
isDev && !isNetlifyLive ? "atlast_session_dev" : "atlast_session";
63
+
const cookieFlags = isSecure
64
+
? `HttpOnly; SameSite=Lax; Max-Age=${CONFIG.COOKIE_MAX_AGE}; Path=/; Secure`
65
+
: `HttpOnly; SameSite=Lax; Max-Age=${CONFIG.COOKIE_MAX_AGE}; Path=/`;
66
+
67
+
console.log(
68
+
"[oauth-callback] Setting cookie:",
69
+
cookieName,
70
+
"for URL:",
71
+
currentUrl,
72
+
);
57
73
58
74
return redirectResponse(
59
75
`${currentUrl}/?session=${sessionId}`,
+58
-17
netlify/functions/oauth-start.ts
+58
-17
netlify/functions/oauth-start.ts
···
2
2
import { createOAuthClient } from "./infrastructure/oauth/OAuthClientFactory";
3
3
import { successResponse } from "./utils";
4
4
import { withErrorHandling } from "./core/middleware";
5
-
import { ValidationError } from "./core/errors";
6
-
import { CONFIG } from "./core/config/constants";
5
+
import { ValidationError, ApiError } from "./core/errors";
7
6
8
7
interface OAuthStartRequestBody {
9
8
login_hint?: string;
···
13
12
const oauthStartHandler: SimpleHandler = async (event) => {
14
13
let loginHint: string | undefined = undefined;
15
14
16
-
if (event.body) {
17
-
const parsed: OAuthStartRequestBody = JSON.parse(event.body);
18
-
loginHint = parsed.login_hint;
19
-
}
15
+
try {
16
+
if (event.body) {
17
+
const parsed: OAuthStartRequestBody = JSON.parse(event.body);
18
+
loginHint = parsed.login_hint;
19
+
}
20
20
21
-
if (!loginHint) {
22
-
throw new ValidationError("login_hint (handle or DID) is required");
23
-
}
21
+
if (!loginHint) {
22
+
throw new ValidationError("login_hint (handle or DID) is required");
23
+
}
24
24
25
-
console.log("[oauth-start] Starting OAuth flow for:", loginHint);
25
+
console.log("[oauth-start] Starting OAuth flow for:", loginHint);
26
26
27
-
const client = await createOAuthClient(event);
27
+
let client;
28
+
try {
29
+
client = await createOAuthClient(event);
30
+
console.log("[oauth-start] OAuth client created successfully");
31
+
} catch (clientError) {
32
+
console.error(
33
+
"[oauth-start] Failed to create OAuth client:",
34
+
clientError instanceof Error
35
+
? clientError.message
36
+
: String(clientError),
37
+
);
38
+
throw new ApiError(
39
+
"Failed to create OAuth client",
40
+
500,
41
+
clientError instanceof Error ? clientError.message : "Unknown error",
42
+
);
43
+
}
28
44
29
-
const authUrl = await client.authorize(loginHint, {
30
-
scope: CONFIG.OAUTH_SCOPES,
31
-
});
45
+
let authUrl;
46
+
try {
47
+
authUrl = await client.authorize(loginHint, {
48
+
scope: "atproto transition:generic",
49
+
});
50
+
console.log("[oauth-start] Generated auth URL successfully");
51
+
} catch (authorizeError) {
52
+
console.error(
53
+
"[oauth-start] Failed to authorize:",
54
+
authorizeError instanceof Error
55
+
? authorizeError.message
56
+
: String(authorizeError),
57
+
);
58
+
throw new ApiError(
59
+
"Failed to generate authorization URL",
60
+
500,
61
+
authorizeError instanceof Error
62
+
? authorizeError.message
63
+
: "Unknown error",
64
+
);
65
+
}
32
66
33
-
console.log("[oauth-start] Generated auth URL for:", loginHint);
34
-
35
-
return successResponse({ url: authUrl.toString() });
67
+
console.log("[oauth-start] Returning auth URL for:", loginHint);
68
+
return successResponse({ url: authUrl.toString() });
69
+
} catch (error) {
70
+
// This will be caught by withErrorHandling, but log it here too for clarity
71
+
console.error(
72
+
"[oauth-start] Top-level error:",
73
+
error instanceof Error ? error.message : String(error),
74
+
);
75
+
throw error;
76
+
}
36
77
};
37
78
38
79
export const handler = withErrorHandling(oauthStartHandler);
+27
-14
netlify/functions/services/SessionService.ts
+27
-14
netlify/functions/services/SessionService.ts
···
40
40
console.log("[SessionService] Found user session for DID:", did);
41
41
42
42
// Cache the OAuth client per session for 5 minutes
43
-
const cacheKey = `oauth-client-${sessionId}`;
43
+
const host = event.headers?.host || "default";
44
+
const cacheKey = `oauth-client-${sessionId}-${host}`;
44
45
let client = configCache.get(cacheKey) as NodeOAuthClient | null;
45
46
46
47
if (!client) {
···
51
52
console.log("[SessionService] Using cached OAuth client");
52
53
}
53
54
54
-
const oauthSession = await client.restore(did);
55
-
console.log("[SessionService] Restored OAuth session for DID:", did);
55
+
try {
56
+
const oauthSession = await client.restore(did);
57
+
console.log("[SessionService] Restored OAuth session for DID:", did);
58
+
59
+
// Log token rotation for monitoring
60
+
// The restore() call automatically refreshes if needed
61
+
const sessionData = await sessionStore.get(did);
62
+
if (sessionData) {
63
+
// Token refresh happens transparently in restore()
64
+
// Just log for monitoring purposes
65
+
console.log("[SessionService] OAuth session restored/refreshed");
66
+
}
56
67
57
-
// Log token rotation for monitoring
58
-
// The restore() call automatically refreshes if needed
59
-
const sessionData = await sessionStore.get(did);
60
-
if (sessionData) {
61
-
// Token refresh happens transparently in restore()
62
-
// Just log for monitoring purposes
63
-
console.log("[SessionService] OAuth session restored/refreshed");
68
+
const agent = new Agent(oauthSession);
69
+
return { agent, did, client };
70
+
} catch (error) {
71
+
console.error(
72
+
"[SessionService] Failed to restore session:",
73
+
error instanceof Error ? error.message : String(error),
74
+
);
75
+
// Clear the cached client if restore fails - it might be stale or misconfigured
76
+
configCache.delete(cacheKey);
77
+
throw new AuthenticationError(
78
+
"Failed to restore OAuth session",
79
+
error instanceof Error ? error.message : "Session restoration failed",
80
+
);
64
81
}
65
-
66
-
const agent = new Agent(oauthSession);
67
-
68
-
return { agent, did, client };
69
82
}
70
83
71
84
static async deleteSession(
+12
-4
src/index.css
+12
-4
src/index.css
···
19
19
font-family: "Rubik", "Fira Sans", sans-serif;
20
20
}
21
21
22
+
html,
23
+
body {
24
+
min-height: 100vh;
25
+
min-height: -webkit-fill-available;
26
+
}
27
+
22
28
body {
23
29
@apply bg-gradient-to-br
24
-
from-cyan-50 via-purple-50 to-pink-50
25
-
dark:from-indigo-950 dark:via-purple-900 dark:to-slate-900
26
-
text-slate-900 dark:text-slate-100
27
-
transition-colors duration-300;
30
+
from-cyan-50 via-purple-50 to-pink-50
31
+
dark:from-indigo-950 dark:via-purple-900 dark:to-slate-900
32
+
text-slate-900 dark:text-slate-100
33
+
transition-colors duration-300;
34
+
padding: env(safe-area-inset-top) env(safe-area-inset-right)
35
+
env(safe-area-inset-bottom) env(safe-area-inset-left);
28
36
}
29
37
30
38
button {