+30
-30
dist/index.html
+30
-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-BmU3Lkw-.js"></script>
25
-
<link rel="stylesheet" crossorigin href="/assets/index-DQCpc624.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 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>
+37
-54
netlify/functions/infrastructure/oauth/config.ts
+37
-54
netlify/functions/infrastructure/oauth/config.ts
···
6
6
export function getOAuthConfig(event?: {
7
7
headers: Record<string, string | undefined>;
8
8
}): OAuthConfig {
9
-
const host = event?.headers?.host || "default";
10
-
const cacheKey = `oauth-config-${host}`;
9
+
// 1. Determine host dynamically
10
+
const host = event?.headers?.host;
11
+
const cacheKey = `oauth-config-${host || "default"}`;
11
12
12
13
const cached = configCache.get(cacheKey) as OAuthConfig | undefined;
13
14
if (cached) {
···
15
16
}
16
17
17
18
let baseUrl: string | undefined;
18
-
let deployContext: string | undefined;
19
19
20
-
if (event?.headers) {
21
-
deployContext = event.headers["x-nf-deploy-context"];
22
-
const forwardedProto = event.headers["x-forwarded-proto"] || "https";
23
-
24
-
if (host && !host.includes("localhost") && !host.includes("127.0.0.1")) {
25
-
baseUrl = `${forwardedProto}://${host}`;
26
-
}
27
-
}
28
-
29
-
if (!baseUrl) {
30
-
baseUrl = process.env.DEPLOY_URL || process.env.URL;
31
-
}
32
-
33
-
console.log("🔍 OAuth Config:", {
34
-
fromHost: event?.headers?.host,
35
-
deployContext: deployContext || process.env.CONTEXT,
36
-
baseUrl,
37
-
envAvailable: {
38
-
DEPLOY_URL: !!process.env.DEPLOY_URL,
39
-
URL: !!process.env.URL,
40
-
},
41
-
});
20
+
// 2. Determine if local based on host header
21
+
const isLocal =
22
+
!host || host.includes("localhost") || host.includes("127.0.0.1");
42
23
43
-
const isLocalhost =
44
-
!baseUrl || baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1");
24
+
// 3. Local oauth config
25
+
if (isLocal) {
26
+
const currentHost = host || "localhost:8888";
27
+
const protocol = currentHost.includes("127.0.0.1")
28
+
? "http://127.0.0.1"
29
+
: "http://localhost";
45
30
46
-
let config: OAuthConfig;
31
+
// Redirect URI must use host in address bar
32
+
const redirectUri = `${protocol}:${currentHost.split(":")[1] || "8888"}/.netlify/functions/oauth-callback`;
47
33
48
-
if (isLocalhost) {
49
-
const port = process.env.PORT || "8888";
34
+
// ClientID must start with localhost
35
+
// but redirect_uri query inside must match actual redirectUri
50
36
const clientId = `http://localhost?${new URLSearchParams([
51
-
[
52
-
"redirect_uri",
53
-
`http://127.0.0.1:${port}/.netlify/functions/oauth-callback`,
54
-
],
37
+
["redirect_uri", redirectUri],
55
38
["scope", CONFIG.OAUTH_SCOPES],
56
39
])}`;
57
40
58
41
console.log("Using loopback OAuth for local development");
59
42
60
-
config = {
43
+
const config: OAuthConfig = {
61
44
clientId: clientId,
62
-
redirectUri: `http://127.0.0.1:${port}/.netlify/functions/oauth-callback`,
45
+
redirectUri: redirectUri,
63
46
jwksUri: undefined,
64
47
clientType: "loopback",
65
48
};
66
-
} else {
67
-
if (!baseUrl) {
68
-
throw new ApiError(
69
-
"No public URL available for OAuth configuration",
70
-
500,
71
-
"Missing DEPLOY_URL or URL environment variables.",
72
-
);
73
-
}
49
+
50
+
configCache.set(cacheKey, config, 300000);
51
+
return config;
52
+
}
53
+
54
+
// 4. Production oauth config
55
+
console.log("Using confidential OAuth client for:", baseUrl);
74
56
75
-
console.log("Using confidential OAuth client for:", baseUrl);
57
+
const forwardedProto = event?.headers?.["x-forwarded-proto"] || "https";
58
+
baseUrl = host
59
+
? `${forwardedProto}://${host}`
60
+
: process.env.DEPLOY_URL || process.env.URL;
76
61
77
-
config = {
78
-
clientId: `${baseUrl}/oauth-client-metadata.json`,
79
-
redirectUri: `${baseUrl}/.netlify/functions/oauth-callback`,
80
-
jwksUri: `${baseUrl}/.netlify/functions/jwks`,
81
-
clientType: "discoverable",
82
-
usePrivateKey: true,
83
-
};
84
-
}
62
+
const config: OAuthConfig = {
63
+
clientId: `${baseUrl}/oauth-client-metadata.json`,
64
+
redirectUri: `${baseUrl}/.netlify/functions/oauth-callback`,
65
+
jwksUri: `${baseUrl}/.netlify/functions/jwks`,
66
+
clientType: "discoverable",
67
+
usePrivateKey: true,
68
+
};
85
69
86
70
configCache.set(cacheKey, config, 300000);
87
-
88
71
return config;
89
72
}
+1
-1
netlify/functions/logout.ts
+1
-1
netlify/functions/logout.ts
+6
-4
netlify/functions/oauth-callback.ts
+6
-4
netlify/functions/oauth-callback.ts
···
11
11
const config = getOAuthConfig(event);
12
12
const isDev = config.clientType === "loopback";
13
13
14
-
let currentUrl = isDev
15
-
? config.redirectUri.replace("/.netlify/functions/oauth-callback", "")
16
-
: config.redirectUri.replace("/.netlify/functions/oauth-callback", "");
14
+
// Land back on the same host you started from
15
+
let currentUrl = config.redirectUri.replace(
16
+
"/.netlify/functions/oauth-callback",
17
+
"",
18
+
);
17
19
18
20
const params = new URLSearchParams(event.rawUrl.split("?")[1] || "");
19
21
const code = params.get("code");
···
29
31
return redirectResponse(`${currentUrl}/?error=Missing OAuth parameters`);
30
32
}
31
33
32
-
const client = await createOAuthClient();
34
+
const client = await createOAuthClient(event);
33
35
34
36
const result = await client.callback(params);
35
37
+5
-2
netlify/functions/services/SessionService.ts
+5
-2
netlify/functions/services/SessionService.ts
···
68
68
return { agent, did, client };
69
69
}
70
70
71
-
static async deleteSession(sessionId: string): Promise<void> {
71
+
static async deleteSession(
72
+
sessionId: string,
73
+
event?: HandlerEvent,
74
+
): Promise<void> {
72
75
console.log("[SessionService] Deleting session:", sessionId);
73
76
74
77
const userSession = await userSessions.get(sessionId);
···
80
83
const did = userSession.did;
81
84
82
85
try {
83
-
const client = await createOAuthClient();
86
+
const client = await createOAuthClient(event);
84
87
await client.revoke(did);
85
88
console.log("[SessionService] Revoked OAuth session for DID:", did);
86
89
} catch (error) {