11import { action } from './_generated/server'
22import { v } from 'convex/values'
33
4- // Simple JWT implementation that works in Convex environment
5- function base64UrlEncode ( str : string ) : string {
6- return btoa ( str ) . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = / g, '' )
7- }
8-
9- function createSimpleJWT ( payload : any , secret : string ) : string {
10- const header = {
11- alg : 'HS256' ,
12- typ : 'JWT' ,
13- }
14-
15- const encodedHeader = base64UrlEncode ( JSON . stringify ( header ) )
16- const encodedPayload = base64UrlEncode ( JSON . stringify ( payload ) )
17-
18- // Create signature using a simple HMAC-like approach
19- // Note: This is a simplified version for the Convex environment
20- const data = `${ encodedHeader } .${ encodedPayload } `
21- const signature = base64UrlEncode ( secret + data )
22-
23- return `${ encodedHeader } .${ encodedPayload } .${ signature } `
24- }
25-
264// Helper function to convert base64 to Uint8Array (works in Convex)
275function base64ToUint8Array ( base64 : string ) : Uint8Array {
286 const binaryString = atob ( base64 )
@@ -90,27 +68,24 @@ export const deployToNetlify = action({
9068
9169 // Get Netlify credentials from environment
9270 const netlifyToken = process . env . NETLIFY_TOKEN
93- const oauthClientId = process . env . NETLIFY_OAUTH_CLIENT_ID
94- const oauthClientSecret = process . env . NETLIFY_OAUTH_CLIENT_SECRET
9571 const netlifyTeamSlug = process . env . NETLIFY_TEAM_SLUG
9672
9773 if ( ! netlifyToken ) {
9874 throw new Error ( 'NETLIFY_TOKEN not configured' )
9975 }
100- if ( ! oauthClientId || ! oauthClientSecret ) {
101- throw new Error (
102- 'NETLIFY_OAUTH_CLIENT_ID and NETLIFY_OAUTH_CLIENT_SECRET must be configured for claimable sites'
103- )
104- }
105-
106- // Generate a unique session ID for this deployment
107- const sessionId = `forge-${ Date . now ( ) } -${ Math . random ( )
108- . toString ( 36 )
109- . substr ( 2 , 9 ) } `
11076
11177 try {
112- // Step 1: Create a new site with metadata for claimable sites
113- // Using created_via and session_id allows users to claim the site later
78+ // Step 1: Create a new site
79+ const sitePayload : any = {
80+ name : `${ args . siteName } -${ Date . now ( ) } ` ,
81+ custom_domain : null ,
82+ }
83+
84+ // Optional: specify team
85+ if ( netlifyTeamSlug ) {
86+ sitePayload . account_slug = netlifyTeamSlug
87+ }
88+
11489 const createSiteResponse = await fetch (
11590 'https://api.netlify.com/api/v1/sites' ,
11691 {
@@ -119,13 +94,7 @@ export const deployToNetlify = action({
11994 Authorization : `Bearer ${ netlifyToken } ` ,
12095 'Content-Type' : 'application/json' ,
12196 } ,
122- body : JSON . stringify ( {
123- name : `${ args . siteName } -${ Date . now ( ) } ` ,
124- custom_domain : null ,
125- created_via : 'TanStack Forge' , // Important for claimable sites
126- session_id : sessionId , // Session ID that will be used in the JWT
127- account_slug : netlifyTeamSlug , // Optional: specify team
128- } ) ,
97+ body : JSON . stringify ( sitePayload ) ,
12998 }
13099 )
131100
@@ -173,32 +142,49 @@ export const deployToNetlify = action({
173142
174143 console . log ( 'Build triggered successfully:' , build )
175144
176- // Step 3: Create a signed JWT claim URL
177- // The JWT contains the OAuth client ID and session ID
178- const claimToken = createSimpleJWT (
179- {
180- client_id : oauthClientId ,
181- session_id : sessionId ,
182- } ,
183- oauthClientSecret !
184- )
185-
186- // The claim URL uses a hash fragment with the JWT token
187- const claimUrl = `https://app.netlify.com/claim#${ claimToken } `
188-
189- console . log ( 'Claimable site created with signed claim URL' )
145+ // Step 3: Generate a claim URL using the Netlify API
146+ // Note: The PAT used here must be associated with an OAuth app to generate claim URLs
147+ let claimUrl : string | undefined = undefined
148+
149+ try {
150+ const claimResponse = await fetch (
151+ `https://api.netlify.com/api/v1/sites/${ siteId } /claim` ,
152+ {
153+ method : 'POST' ,
154+ headers : {
155+ Authorization : `Bearer ${ netlifyToken } ` ,
156+ 'Content-Type' : 'application/json' ,
157+ } ,
158+ body : JSON . stringify ( { } ) ,
159+ }
160+ )
161+
162+ if ( claimResponse . ok ) {
163+ const claimData = await claimResponse . json ( )
164+ claimUrl = claimData . claim_url
165+ console . log ( 'Claimable site created with claim URL' )
166+ } else {
167+ const error = await claimResponse . text ( )
168+ console . log (
169+ 'Could not generate claim URL (PAT may not be associated with an OAuth app):' ,
170+ error
171+ )
172+ }
173+ } catch ( error ) {
174+ console . log ( 'Failed to generate claim URL:' , error )
175+ }
190176
191177 // Return the deployed site URL and build information
192178 return {
193179 url : site . ssl_url || site . url || `https://${ site . name } .netlify.app` ,
194180 adminUrl : site . admin_url ,
195- claimUrl : claimUrl , // Signed JWT URL for users to claim the site
181+ claimUrl : claimUrl , // URL for users to claim the site (or undefined if claim endpoint failed)
196182 siteId : siteId ,
197183 deployId : build . deploy_id ,
198184 buildId : build . id ,
199185 siteName : site . name ,
200186 buildStatus : build . state , // 'building', 'ready', 'error'
201- isClaimable : true , // Indicates this is a claimable site
187+ isClaimable : ! ! claimUrl , // Indicates if claim URL was successfully generated
202188 }
203189 } catch ( error ) {
204190 console . error ( 'Netlify deployment error:' , error )
0 commit comments